Skip to content

Cpp optis#447

Open
shubhammalhotra28 wants to merge 14 commits intomainfrom
Rag+CPP_optis
Open

Cpp optis#447
shubhammalhotra28 wants to merge 14 commits intomainfrom
Rag+CPP_optis

Conversation

@shubhammalhotra28
Copy link
Contributor

@shubhammalhotra28 shubhammalhotra28 commented Mar 6, 2026

Description

Brief description of the changes made.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring

Testing

  • Lint passes locally
  • Added/updated tests for changes

Platform-Specific Testing (check all that apply)

Swift SDK / iOS Sample:

  • Tested on iPhone (Simulator or Device)
  • Tested on iPad / Tablet
  • Tested on Mac (macOS target)

Kotlin SDK / Android Sample:

  • Tested on Android Phone (Emulator or Device)
  • Tested on Android Tablet

Flutter SDK / Flutter Sample:

  • Tested on iOS
  • Tested on Android

React Native SDK / React Native Sample:

  • Tested on iOS
  • Tested on Android

Playground:

  • Tested on target platform
  • Verified no regressions in existing Playground projects
    Web SDK / Web Sample:
  • Tested in Chrome (Desktop)
  • Tested in Firefox
  • Tested in Safari
  • WASM backends load (LlamaCpp + ONNX)
  • OPFS storage persistence verified (survives page refresh)
  • Settings persistence verified (localStorage)

Labels

Please add the appropriate label(s):

SDKs:

  • Swift SDK - Changes to Swift SDK (sdk/runanywhere-swift)
  • Kotlin SDK - Changes to Kotlin SDK (sdk/runanywhere-kotlin)
  • Flutter SDK - Changes to Flutter SDK (sdk/runanywhere-flutter)
  • React Native SDK - Changes to React Native SDK (sdk/runanywhere-react-native)
  • Web SDK - Changes to Web SDK (sdk/runanywhere-web)
  • Commons - Changes to shared native code (sdk/runanywhere-commons)

Sample Apps:

  • iOS Sample - Changes to iOS example app (examples/ios)
  • Android Sample - Changes to Android example app (examples/android)
  • Flutter Sample - Changes to Flutter example app (examples/flutter)
  • React Native Sample - Changes to React Native example app (examples/react-native)
  • Web Sample - Changes to Web example app (examples/web)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)

Screenshots

Attach relevant UI screenshots for changes (if applicable):

  • Mobile (Phone)
  • Tablet / iPad
  • Desktop / Mac

Summary by CodeRabbit

  • New Features

    • Native cross-platform archive extraction, centralized file management, and a download orchestrator; runtime LoRA load/remove/registry APIs exposed to higher-level SDKs.
  • Improvements

    • Safer memory/threading, more robust error handling, reduced logging overhead; SDKs delegate extraction and file operations to native core for consistent behavior.
  • Bug Fixes / Tests

    • Added integration/unit tests covering extraction and download orchestration.
  • Chores

    • Removed several language-specific archive libraries; bumped minimum C++ requirement to C++20.

Greptile Summary

This PR consolidates platform-specific archive extraction (previously in Swift SWCompression, Kotlin Commons Compress, and Flutter's archive package) into a single native C++ implementation using libarchive, and centralizes file management and download orchestration business logic that was duplicated across all four SDK platforms. It also introduces a native RAG pipeline (ONNX embedding provider + USearch vector store), cache-line-optimized energy VAD, and a significantly improved structured logging system with atomic level-filtering.

Key changes:

  • New rac_extraction.cpp: Streaming libarchive extraction with zip-slip protection, macOS resource fork skipping, and safe symlink handling — well implemented.
  • New download_orchestrator.cpp: Single-file orchestration (rac_download_orchestrate) is sound; however, the multi-file variant (rac_download_orchestrate_multi) has a critical race condition where downloads are started asynchronously but the task is marked complete immediately after the loop, and transfer failures are silently ignored.
  • New file_manager.cpp: Clean centralization of recursive directory sizing and folder lifecycle management via platform I/O callbacks.
  • New RAG components: onnx_embedding_provider.cpp adds WordPiece tokenization with NEON SIMD path and LRU token cache; vector_store_usearch.cpp adds an f16-quantized USearch index. The load() method on the vector store does not clear existing index data before loading, which can produce an inconsistent index/metadata state.
  • Logger refactor: New macros pre-check log level before constructing metadata structs or formatting strings, eliminating overhead for filtered messages using atomic reads.
  • C++ standard bumped to C++20 across all CMakeLists.txt files.

Confidence Score: 3/5

  • Safe to merge the extraction, file manager, logger, and VAD changes; the multi-file download orchestration has a critical correctness issue that should be resolved before merging.
  • The majority of the changes (libarchive extraction, file manager, logger, energy VAD, single-file download orchestration) are well-implemented and safe. The score is held back by two logic bugs: (1) rac_download_orchestrate_multi treats async HTTP launches as synchronous completions, silently dropping transfer-phase failures and marking the task done prematurely, and (2) VectorStoreUSearch::load() can produce inconsistent index/metadata state by not clearing existing data first. These are non-trivial correctness issues in new code paths.
  • sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp (multi-file race condition and incorrect success return) and sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp (load without prior clear).

Important Files Changed

Filename Overview
sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp New download orchestration layer — single-file flow is sound, but rac_download_orchestrate_multi has a critical race: downloads are started asynchronously via rac_http_download yet the task is marked complete immediately after the loop, and any_failed only reflects launch errors, not transfer failures. The function also incorrectly returns RAC_SUCCESS when required files fail to start.
sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp Clean, well-structured libarchive extraction with solid zip-slip protection, macOS resource fork skipping, safe symlink validation, and streaming constant-memory reads. Error handling is thorough and cleanup paths are correct.
sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp Pimpl USearch vector store with good mutex protection and monotonic key generation. The load() method does not clear existing index data before loading, which can leave the USearch index out of sync with the newly deserialized metadata maps.
sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp Comprehensive ONNX embedding provider with LRU token cache, SIMD-optimized ARM tokenizer, dynamic batch padding, and RAII ORT guards. Locking contract between embed() and embed_batch() is functional but fragile due to dual-path mutex acquisition.
sdk/runanywhere-commons/src/core/rac_logger.cpp Refactored logger using atomic reads for the hot-path level check, avoiding mutex contention on every log call. Dual stderr+platform-adapter output path is well handled.
sdk/runanywhere-commons/src/infrastructure/file_management/file_manager.cpp Centralized file management consolidating recursive directory size calculation and folder lifecycle across all platform SDKs into a single C++ implementation with platform I/O callbacks. Clean and well-structured.
sdk/runanywhere-commons/src/features/vad/energy_vad.cpp Energy VAD ported from Swift with cache-line alignment groups on hot-path fields, a pre-computed squared threshold to avoid sqrt in the hot path, and a static_assert to catch layout regressions. Well-optimized.
sdk/runanywhere-commons/tests/test_download_orchestrator.cpp New unit tests cover rac_download_requires_extraction, rac_download_compute_destination, and rac_find_model_path_after_extraction. Uses shell commands (system("rm -rf ...")) inside test helpers — functional but worth noting for environments where shell access is restricted.
sdk/runanywhere-commons/tests/test_extraction.cpp Comprehensive extraction tests including zip-slip protection, magic byte detection, options, and progress callbacks. Good coverage of the new libarchive extraction path.

Sequence Diagram

sequenceDiagram
    participant SDK as Swift/Kotlin/Flutter/RN SDK
    participant Orch as DownloadOrchestrator (C++)
    participant DM as DownloadManager
    participant HTTP as Platform HTTP Adapter
    participant Ext as Extraction (libarchive)
    participant FM as FileManager (C++)

    SDK->>Orch: rac_download_orchestrate(model_id, url, ...)
    Orch->>DM: rac_download_manager_start(task_id)
    Orch->>HTTP: rac_http_download(url, dest, callbacks)
    HTTP-->>Orch: orchestrate_http_progress(bytes, total)
    Orch->>DM: rac_download_manager_update_progress()
    HTTP-->>Orch: orchestrate_http_complete(result, path)
    alt needs_extraction
        Orch->>DM: mark_complete (→ EXTRACTING state)
        Orch->>Ext: rac_extract_archive_native(archive, dest_dir)
        Ext-->>Orch: extraction_result
        Orch->>Orch: rac_find_model_path_after_extraction()
        Orch->>Orch: delete_file(archive)
        Orch->>DM: mark_extraction_complete(final_path)
    else direct download
        Orch->>DM: mark_complete(final_path)
    end
    Orch->>SDK: user_complete_callback(task_id, result, path)

    Note over SDK,FM: File management (separate flow)
    SDK->>FM: rac_file_manager_calculate_storage_usage()
    FM->>FM: recursive directory traversal via callbacks
    FM-->>SDK: storage_info (used/available/warning)
Loading

Comments Outside Diff (2)

  1. sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp, line 317-373 (link)

    load() does not clear existing index data before loading

    load() calls index_.load(path.c_str()) while holding mutex_, but it does not call index_.clear() first. If the VectorStoreUSearch already contains entries (from prior add_chunk calls or a previous load), the USearch index may retain stale vectors while the metadata maps (chunks_, id_to_key_) are completely replaced by the freshly deserialized data. This leaves the index and metadata maps in an inconsistent state — old vectors in the index with no corresponding metadata entries — which would cause key-not-found errors in search().

    Add an explicit clear of the USearch index before loading:

    bool load(const std::string& path) {
        std::lock_guard<std::mutex> lock(mutex_);
        
        // Clear existing state first to prevent stale index/metadata inconsistency
        index_.clear();
        chunks_.clear();
        id_to_key_.clear();
        next_key_ = 0;
    
        auto load_result = index_.load(path.c_str());
        // ...
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp
    Line: 317-373
    
    Comment:
    **`load()` does not clear existing index data before loading**
    
    `load()` calls `index_.load(path.c_str())` while holding `mutex_`, but it does not call `index_.clear()` first. If the `VectorStoreUSearch` already contains entries (from prior `add_chunk` calls or a previous `load`), the USearch index may retain stale vectors while the metadata maps (`chunks_`, `id_to_key_`) are completely replaced by the freshly deserialized data. This leaves the index and metadata maps in an inconsistent state — old vectors in the index with no corresponding metadata entries — which would cause key-not-found errors in `search()`.
    
    Add an explicit clear of the USearch index before loading:
    ```cpp
    bool load(const std::string& path) {
        std::lock_guard<std::mutex> lock(mutex_);
        
        // Clear existing state first to prevent stale index/metadata inconsistency
        index_.clear();
        chunks_.clear();
        id_to_key_.clear();
        next_key_ = 0;
    
        auto load_result = index_.load(path.c_str());
        // ...
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp, line 748-789 (link)

    embed_batch acquires lock only for batches > 1, creating inconsistent locking contract

    When texts.size() == 1, embed_batch returns early by delegating to embed(), which internally acquires embed_mutex_. For all larger batches, embed_batch itself acquires embed_mutex_ (line 763) before calling embed_sub_batch (which does not lock). This creates an inconsistent locking pattern:

    • Concurrent calls with size == 1 are serialized by embed_mutex_ inside embed()
    • Concurrent calls with size > 1 are serialized by embed_mutex_ inside embed_batch()
    • A concurrent embed() call and embed_batch(size > 1) call are correctly serialized

    While this happens to be race-free, the dual-path locking is fragile: the pre-allocated buffers (input_ids_buf_, attention_mask_buf_, token_type_ids_buf_) are shared between embed() and embed_sub_batch(). If embed_sub_batch is ever called without embed_mutex_ held (e.g., from a new code path), it would race. Consider moving the lock acquisition to embed_sub_batch itself, or documenting clearly that embed_sub_batch must always be called under embed_mutex_.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp
    Line: 748-789
    
    Comment:
    **`embed_batch` acquires lock only for batches > 1, creating inconsistent locking contract**
    
    When `texts.size() == 1`, `embed_batch` returns early by delegating to `embed()`, which internally acquires `embed_mutex_`. For all larger batches, `embed_batch` itself acquires `embed_mutex_` (line 763) before calling `embed_sub_batch` (which does not lock). This creates an inconsistent locking pattern:
    
    - Concurrent calls with `size == 1` are serialized by `embed_mutex_` inside `embed()`- Concurrent calls with `size > 1` are serialized by `embed_mutex_` inside `embed_batch()`- **A concurrent `embed()` call and `embed_batch(size > 1)` call are correctly serialized** ✓
    
    While this happens to be race-free, the dual-path locking is fragile: the pre-allocated buffers (`input_ids_buf_`, `attention_mask_buf_`, `token_type_ids_buf_`) are shared between `embed()` and `embed_sub_batch()`. If `embed_sub_batch` is ever called without `embed_mutex_` held (e.g., from a new code path), it would race. Consider moving the lock acquisition to `embed_sub_batch` itself, or documenting clearly that `embed_sub_batch` must always be called under `embed_mutex_`.
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 67b4c22

Greptile also left 3 inline comments on this PR.

sanchitmonga22 and others added 10 commits February 27, 2026 15:20
- Implemented Download Orchestrator for high-level model download lifecycle management, consolidating logic from various SDKs.
- Introduced File Manager for centralized file management operations, including directory creation, size calculation, and cache management.
- Updated GeneratedPluginRegistrant for new plugins: CameraAndroidCameraxPlugin and ImagePickerPlugin.
- Cleaned up and organized existing code for better maintainability.
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9cedf0d9-031e-48c2-a910-597d14dcbdeb

📥 Commits

Reviewing files that changed from the base of the PR and between 6b38fb0 and be140be.

📒 Files selected for processing (1)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart

📝 Walkthrough

Walkthrough

Moves archive extraction and file-management into the native C++ core (libarchive), adds a download orchestrator and file-manager API, removes language-level archive libs, introduces JNI/FFI/Swift/Kotlin/Dart bridges, many new C APIs, thread-safety and OOM hardening, and bumps C++ standard to C++20.

Changes

Cohort / File(s) Summary
Build / Standards & docs
Playground/.../CMakeLists.txt, sdk/.../CMakeLists.txt, sdk/runanywhere-web/wasm/CMakeLists.txt, sdk/runanywhere-commons/.clang-format, Playground/README.md
Bump C++ standard from 17→20 across targets and update related docs/configs.
SwiftPM / Package pins
examples/ios/.../Package.resolved, Package.resolved
Removed three SwiftPM remote pins (bitbytedata, swcompression, zipfoundation); originHash updated.
Native extraction (libarchive)
sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h, sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp, sdk/runanywhere-commons/tests/test_extraction.cpp
Add rac_extract_archive_native, rac_detect_archive_type, streaming constant-memory extraction, security checks, progress/results, and tests.
Download orchestrator
sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download_orchestrator.h, sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp, sdk/runanywhere-commons/tests/test_download_orchestrator.cpp
New rac_download_orchestrate/*_multi, destination computation, post-extraction path resolution, multi-file support, orchestration logic and tests.
File manager API & implementations
sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h, .../file_manager.cpp, sdk/runanywhere-react-native/.../bridges/FileManagerBridge.*, .../FileManagerBridge.hpp
Add rac_file_manager callbacks contract, storage info types, C++/POSIX/Swift/Kotlin/Dart bridge implementations and operations (create/delete folders, size, clear cache/temp, storage checks).
Download manager extraction lifecycle
sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download.h, .../download_manager.cpp
Add rac_download_manager_mark_extraction_complete and rac_download_manager_mark_extraction_failed and integrate extraction-state notifications.
Bridges / JNI / FFI / SDK wiring
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp, sdk/runanywhere-kotlin/.../RunAnywhereBridge.kt, sdk/runanywhere-flutter/.../dart_bridge_*.dart, sdk/runanywhere-swift/.../CppBridge+*.swift, examples/flutter/.../GeneratedPluginRegistrant.*
Expose new rac_* APIs via JNI/FFI/Swift/Kotlin/Dart; add many JNI exports and platform registrations; implement Dart/Swift/Kotlin bridge helpers.
Language SDK removals & delegation
sdk/runanywhere-swift/.../ArchiveUtility.swift, sdk/runanywhere-react-native/.../ArchiveUtility.kt, sdk/runanywhere-flutter/pubspec.yaml, sdk/runanywhere-react-native/.../build.gradle
Remove in-language archive/extraction implementations and dependencies (SWCompression, ZIPFoundation, archive, Apache Commons, react-native-zip-archive); delegate extraction and model-path logic to native C++.
Exports, versions & lockfiles
sdk/runanywhere-commons/exports/RACommons.exports, sdk/runanywhere-commons/VERSIONS
Export many new rac_* symbols, add LIBARCHIVE_VERSION=3.8.1 constant, and update exports.
Core robustness & logging
sdk/runanywhere-commons/include/rac/core/rac_logger.h, sdk/runanywhere-commons/src/core/rac_logger.cpp, rac_structured_error.cpp
Atomic logger state, early-out level checks, safer JSON escaping/clamping, many defensive OOM checks and safer string handling.
Lifecycle & concurrency
sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp, include/rac/core/capabilities/rac_lifecycle.h
Add rac_lifecycle_acquire_service / rac_lifecycle_release_service; atomic service pins and wait-on-release unload semantics.
Backends, features & misc fixes
sdk/runanywhere-commons/src/backends/..., src/features/..., sdk/runanywhere-commons/src/infrastructure/...
Logging migration to RAC_LOG_*, sampler caching, UTF‑8 safety, atomic flags, mutexes, cache-line layout for VAD, RNG thread-localization, numerous OOM and error-handling hardenings.
Flutter / Dart changes
sdk/runanywhere-flutter/.../dart_bridge_file_manager.dart, dart_bridge_download.dart, runanywhere.dart, native/*
Add Dart FFI bridges for file-manager, download helpers, LoRA FFI, delegate extraction/size/storage to native bridge; introduce new public LoRA APIs/types.
React Native / Kotlin / Swift integration
sdk/runanywhere-react-native/*, sdk/runanywhere-kotlin/*, sdk/runanywhere-swift/*
Add FileManager bridge (C++/JNI/Swift), remove platform archive utilities, add Kotlin CppBridgeFileManager registration and new RunAnywhereBridge native methods.
Tests & CI
sdk/runanywhere-commons/tests/CMakeLists.txt, new test files
Add test_extraction and test_download_orchestrator, update test C++ standard and add coverage for orchestrator/extraction.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Client / SDK
    participant DM as Download Manager
    participant DO as Download Orchestrator
    participant EX as Native Extractor (libarchive)
    participant FM as File Manager (platform callbacks)
    participant MR as Model Registry

    App->>+DM: rac_download_orchestrate(model_id, url, ...)
    DM->>+DO: register task, compute destination
    DO->>FM: ensure dirs / check storage (callbacks)
    DO->>DM: request HTTP download via platform adapter
    DM-->>DO: download complete (archive path)
    DO->>+EX: rac_extract_archive_native(archivePath, destDir, options)
    EX->>FM: write extracted files via platform callbacks
    EX-->>-DO: extraction result (stats)
    DO->>MR: resolve final model path after extraction
    DO-->>-App: completion callback (final path / error)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • shubhammalhotra28

"I hopped through code with tiny paws,
Tuned C++ standards and fixed the flaws.
Libarchive hums, bridges stitch the seams,
Threads and files now dance in sync with dreams.
A carrot-cheer for builds and streams!" 🥕🐇

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Rag+CPP_optis

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (13)
sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp (1)

166-173: ⚠️ Potential issue | 🟠 Major

Roll back the tracker if out_generation_id allocation fails.

Both start paths insert into active_generations before malloc. If that allocation fails, the API returns RAC_ERROR_OUT_OF_MEMORY but leaves a live tracker behind, which can skew analytics state and accumulate stale entries.

🔧 Minimal fix
     handle->active_generations[id] = tracker;

     // Allocate and copy the ID for the caller
     *out_generation_id = static_cast<char*>(malloc(id.size() + 1));
     if (!*out_generation_id) {
+        handle->active_generations.erase(id);
         return RAC_ERROR_OUT_OF_MEMORY;
     }
     memcpy(*out_generation_id, id.c_str(), id.size() + 1);

Also applies to: 205-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp` around lines 166
- 173, The code stores tracker into handle->active_generations[id] before
allocating *out_generation_id; if malloc fails you must roll back that insertion
to avoid stale trackers: after the failed allocation return path, remove the
entry (e.g. handle->active_generations.erase(id)) and release/cleanup tracker if
it's heap-allocated, then return RAC_ERROR_OUT_OF_MEMORY; apply the same
rollback/frees to the second occurrence around the 205–212 block as well.
sdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cpp (1)

114-127: ⚠️ Potential issue | 🟠 Major

Compact failed entries before publishing model_count.

Line 126 skips the record, but out_info->model_count still reports the original registry count. Callers will iterate over a zeroed placeholder entry, so the OOM turns into inconsistent output rather than a true skip. Either compact successful metrics into a separate write index and publish that count, or fail the whole call.

💡 One way to keep the result consistent
-    out_info->model_count = model_count;
+    size_t valid_model_count = 0;
     out_info->total_models_size = 0;

     // Calculate metrics for each model
     for (size_t i = 0; i < model_count; i++) {
         const rac_model_info_t* model = models[i];
-        rac_model_storage_metrics_t* metrics = &out_info->models[i];
+        rac_model_storage_metrics_t* metrics = &out_info->models[valid_model_count];

         // Copy model info
         metrics->model_id = model->id ? strdup(model->id) : nullptr;
         metrics->model_name = model->name ? strdup(model->name) : nullptr;
         if ((model->id && !metrics->model_id) || (model->name && !metrics->model_name)) {
             free(const_cast<char*>(metrics->model_id));
             free(const_cast<char*>(metrics->model_name));
             memset(metrics, 0, sizeof(rac_model_storage_metrics_t));
             continue;
         }

         ...
         out_info->total_models_size += metrics->size_on_disk;
+        valid_model_count++;
     }
+
+    out_info->model_count = valid_model_count;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cpp`
around lines 114 - 127, The loop over model_count writes metrics into
out_info->models but skips failed entries without updating
out_info->model_count, producing inconsistent results; change the loop to use a
separate write index (e.g., size_t write_i = 0) and on success move/assign
metrics into out_info->models[write_i++] (or memcpy/strdup into that slot), and
after the loop set out_info->model_count = write_i (or return an error on OOM
instead of leaving zeroed placeholders); update references to
metrics/model_id/model_name in the loop to write into the write index so only
compacted, valid entries are published.
sdk/runanywhere-commons/src/server/http_server.cpp (1)

116-137: ⚠️ Potential issue | 🟠 Major

Immediate bind failures still sleep through the full startup timeout.

serverThread() only flips running_ after a successful bind. When bind_to_port() fails right away, this loop still sleeps for the full 100 × 100 ms before returning RAC_ERROR_SERVER_BIND_FAILED, so obvious startup failures look hung. Have the worker publish a startup result/state (or signal a condition variable) so start() can return as soon as bind fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 116 - 137,
The start() loop currently sleeps for SERVER_START_POLL_ITERATIONS ×
SERVER_START_POLL_MS even when bind_to_port() in serverThread() fails
immediately; modify startup coordination so serverThread() signals a startup
result (e.g., an atomic startup_result_ enum or a condition variable
startup_cv_) right after bind_to_port() fails or succeeds, and change start() to
wait on that condition variable (or poll startup_result_) with a timeout instead
of blind sleeping. Specifically: add a startup_result_ (or reuse running_) that
serverThread() sets to a distinct error state on bind failure, notify
startup_cv_ after setting it, and in start() replace the for/sleep loop with
startup_cv_.wait_for(...) (or a short-loop checking startup_result_) so start()
can return RAC_ERROR_SERVER_BIND_FAILED immediately; keep cleanup steps
(shouldStop_, join serverThread_, unloadModel(), server_.reset()) the same when
notified of failure.
sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (2)

512-525: ⚠️ Potential issue | 🟠 Major

Clear is_configured before teardown starts.

With rac_voice_agent_is_ready() now doing a lock-free read, the agent still reports ready until Line 524, even though cleanup has already started. Move the store to the top of rac_voice_agent_cleanup() so readiness flips before any component state is torn down.

Suggested change
 std::lock_guard<std::mutex> lock(handle->mutex);

 RAC_LOG_INFO("VoiceAgent", "Cleaning up Voice Agent");
+handle->is_configured.store(false, std::memory_order_release);

 // Cleanup all components (mirrors Swift's cleanup)
 rac_llm_component_cleanup(handle->llm_handle);
 rac_stt_component_cleanup(handle->stt_handle);
 rac_tts_component_cleanup(handle->tts_handle);
 // VAD uses stop + reset instead of cleanup
 rac_vad_component_stop(handle->vad_handle);
 rac_vad_component_reset(handle->vad_handle);

-    handle->is_configured.store(false, std::memory_order_release);
-
     return RAC_SUCCESS;

As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".

Also applies to: 529-535

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around
lines 512 - 525, The readiness flag is flipped too late: in
rac_voice_agent_cleanup() move the atomic write that clears
handle->is_configured (handle->is_configured.store(false,
std::memory_order_release)) to the very start of rac_voice_agent_cleanup(),
before acquiring the mutex and before any component teardown calls
(rac_llm_component_cleanup, rac_stt_component_cleanup,
rac_tts_component_cleanup, rac_vad_component_stop/reset) so that
rac_voice_agent_is_ready() (which does a lock-free read) immediately reports
not-ready; ensure the same change is applied to the other cleanup block
referenced around lines 529-535.

668-799: ⚠️ Potential issue | 🟠 Major

Stage the stream events before invoking the callback.

At Line 669 the mutex stays held across every error/success callback. That can deadlock if the callback re-enters any rac_voice_agent_* API, and it also leaves wav_copy to be allocated only after RAC_VOICE_AGENT_EVENT_AUDIO_SYNTHESIZED has already been emitted. If that late allocation fails, RAC_VOICE_AGENT_EVENT_PROCESSED silently loses audio while the function still returns success. Build/copy both event payloads first, release the mutex, then invoke the callbacks.

As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around
lines 668 - 799, The function holds handle->mutex while invoking callback and
performing late allocations (wav_copy), which can deadlock or lose audio on
allocation failure; change rac_voice_agent_process (the pipeline block using
rac_stt_component_transcribe, rac_llm_component_generate,
rac_tts_component_synthesize, rac_audio_float32_to_wav) to fully build/copy all
event payloads (transcription_event, response_event, audio_event with wav_copy,
processed_event) while still under the lock, then release handle->mutex before
calling callback for any event; ensure wav_copy is allocated/copied before
releasing the lock and ensure all error callbacks are also dispatched after the
lock is released so callbacks cannot re-enter rac_voice_agent_* APIs while the
mutex is held.
sdk/runanywhere-commons/src/features/tts/tts_analytics.cpp (1)

147-153: ⚠️ Potential issue | 🟡 Minor

Avoid leaving a phantom synthesis tracked on OOM.

The tracker is inserted before out_synthesis_id is allocated. If malloc fails here, rac_tts_analytics_start_synthesis returns RAC_ERROR_OUT_OF_MEMORY but leaves an unreachable entry in active_syntheses, which permanently skews this handle’s state.

💡 Suggested fix
-    handle->active_syntheses[id] = tracker;
-
     *out_synthesis_id = static_cast<char*>(malloc(id.size() + 1));
     if (!*out_synthesis_id) {
         return RAC_ERROR_OUT_OF_MEMORY;
     }
     memcpy(*out_synthesis_id, id.c_str(), id.size() + 1);
+    handle->active_syntheses[id] = tracker;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/tts/tts_analytics.cpp` around lines 147
- 153, In rac_tts_analytics_start_synthesis the tracker is stored into
handle->active_syntheses[id] before allocating *out_synthesis_id, so on malloc
failure the phantom tracker remains; fix by either allocating and assigning
*out_synthesis_id (using id.size()+1) before inserting tracker, or if you keep
the current order then on malloc failure free any resources and erase
handle->active_syntheses[id] (and/or delete tracker) before returning
RAC_ERROR_OUT_OF_MEMORY; ensure you reference handle->active_syntheses,
out_synthesis_id, id, tracker and rac_tts_analytics_start_synthesis when making
the change.
sdk/runanywhere-commons/src/features/tts/tts_component.cpp (1)

12-17: ⚠️ Potential issue | 🟡 Minor

Include <new> explicitly in this translation unit.

Line 115 uses new (std::nothrow), but this file does not explicitly include <new>. Relying on transitive includes is brittle and can become a compile break as headers change.

Minimal fix
 `#include` <cstring>
 `#include` <mutex>
+#include <new>
 `#include` <random>
 `#include` <string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 12 -
17, The file tts_component.cpp uses placement new with std::nothrow (new
(std::nothrow) at or around line 115) but does not explicitly include the <new>
header; add an explicit `#include` <new> to the top-of-file includes so the use of
std::nothrow is defined and the translation unit no longer relies on transitive
includes (update the include list near the other standard headers in
tts_component.cpp).
sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)

477-508: ⚠️ Potential issue | 🟠 Major

get_state() / get_metrics() still don't represent the component lifecycle.

Both APIs are derived from a single is_initialized bit, so callers never observe start/stop/reset transitions and the metrics stay stuck at a synthetic “1 successful load”. This needs a dedicated lifecycle state/counter model, not a boolean approximation. As per coding guidelines "Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 477
- 508, The current get_state (rac_vad_component_get_state) and get_metrics
(rac_vad_component_get_metrics) use only component->is_initialized, so they
never expose start/stop/reset transitions or proper counters; add a real
lifecycle model to rac_vad_component (e.g., a rac_lifecycle_state_t state field
and integer counters like total_loads, successful_loads, start_count,
stop_count, reset_count), update all lifecycle transition points
(init/load/start/stop/reset) to set component->state and increment the
appropriate counters under component->mtx, then change
rac_vad_component_get_state to return component->state and
rac_vad_component_get_metrics to populate out_metrics from those counters
(instead of the boolean-based logic) while keeping the same error checks and
mutex usage.
sdk/runanywhere-commons/src/features/vad/energy_vad.cpp (1)

279-320: ⚠️ Potential issue | 🟠 Major

RAC_ERROR_OUT_OF_MEMORY is still bypassed after the nothrow allocation.

new (std::nothrow) rac_energy_vad() only protects the object itself. The later recent_energy_values.resize(...) uses the default allocator, which throws std::bad_alloc on failure. This C API may still throw/terminate instead of returning RAC_ERROR_OUT_OF_MEMORY.

🔧 Possible fix
     rac_energy_vad* vad = new (std::nothrow) rac_energy_vad();
     if (!vad) {
         return RAC_ERROR_OUT_OF_MEMORY;
     }
+
+    try {
+        vad->recent_energy_values.resize(vad->max_recent_values, 0.0f);
+    } catch (const std::bad_alloc&) {
+        delete vad;
+        return RAC_ERROR_OUT_OF_MEMORY;
+    }
 
     // Initialize from config (mirrors Swift init)
     vad->sample_rate = cfg->sample_rate;
@@
-    vad->recent_energy_values.resize(vad->max_recent_values, 0.0f);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/energy_vad.cpp` around lines 279 -
320, The allocation with new (std::nothrow) rac_energy_vad() protects only the
object but recent_energy_values.resize(...) can throw std::bad_alloc; wrap the
resize call in a try/catch that catches std::bad_alloc (or std::exception),
delete the newly created vad on failure and return RAC_ERROR_OUT_OF_MEMORY;
reference the rac_energy_vad allocation, recent_energy_values.resize, and ensure
vad is cleaned up (delete vad) before returning the error so the C API never
lets an exception escape.
sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart (1)

25-35: ⚠️ Potential issue | 🟠 Major

Don’t treat a failed storage check as “enough space.”

Line 31 now depends on the native file-manager bridge, but the catch path still returns true. If that bridge is not registered yet or the FFI call fails, downloads will proceed without any storage guard.

🛡️ Minimal safe fallback
   static Future<bool> checkStorageAvailable({
     required int modelSize,
     double safetyMargin = 0.1,
   }) async {
     try {
       final requiredWithMargin = (modelSize * (1 + safetyMargin)).toInt();
       return DartBridgeFileManager.checkStorage(requiredWithMargin);
     } catch (_) {
-      // Default to available if check fails
-      return true;
+      return false;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart`
around lines 25 - 35, checkStorageAvailable currently treats any exception from
DartBridgeFileManager.checkStorage as "enough space"; change the catch block in
checkStorageAvailable to use a conservative fallback (return false) instead of
true, and log the caught error (include error details and context mentioning
DartBridgeFileManager.checkStorage and requiredWithMargin) so failures of the
native bridge don't allow unsafe downloads; ensure the method still returns a
bool and keeps the requiredWithMargin calculation intact.
sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp (1)

1233-1250: ⚠️ Potential issue | 🟠 Major

Keep registry removal and file deletion in one success path.

Line 1243 removes the registry entry, while Line 1247 deletes files independently of that result. If registry removal fails, this can still delete the files and leave the registry pointing at missing content; if file deletion fails, the method still reports success based only on the registry result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp`
around lines 1233 - 1250, The current deleteModel flow removes the registry
entry then always attempts file deletion and returns only the registry result;
change it so registry removal and file deletion form a single success path: in
HybridRunAnywhereCore::deleteModel, after obtaining modelInfo/framework, call
ModelRegistryBridge::shared().removeModel(modelId) and if that fails do not call
FileManagerBridge::shared().deleteModel and return failure; if removeModel
succeeds, call FileManagerBridge::shared().deleteModel and capture its success;
if file deletion fails, attempt to restore the registry entry using the
previously fetched modelInfo via
ModelRegistryBridge::shared().addModel(modelInfo) (or the appropriate restore
method) and return failure; only return true when both removeModel and file
deletion succeed.
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService+Execution.swift (1)

77-103: ⚠️ Potential issue | 🟠 Major

Add cleanup calls for stored download requests in both success and failure paths.

storeDownloadRequest(downloadRequest, forKey: taskId) at line 77 adds an entry to activeDownloadRequests, but neither the success case (line 83/87) nor the failure case (line 97) removes it. The removeDownloadRequest method exists but is never called, causing stored requests to accumulate indefinitely and creating a resource leak. Add self.removeDownloadRequest(forKey: taskId) before each continuation.resume() call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService`+Execution.swift
around lines 77 - 103, The stored download request added via
storeDownloadRequest(downloadRequest, forKey: taskId) is never removed; call
self.removeDownloadRequest(forKey: taskId) inside the downloadRequest.response
closure immediately before every continuation.resume(returning:) and
continuation.resume(throwing:) so both the success path (where downloadedURL is
non-nil) and all failure branches (invalidResponse and .failure mapped via
mapAlamofireError) remove the entry; update the closure around
withCheckedThrowingContinuation to invoke removeDownloadRequest(forKey: taskId)
just prior to resuming the continuation in each case.
sdk/runanywhere-react-native/packages/core/src/services/FileSystem.ts (1)

285-298: ⚠️ Potential issue | 🟠 Major

modelExists() no longer verifies the expected artifact.

After this change, any non-empty model folder returns true. A failed extraction, stray mmproj, or leftover temp file will now look like a valid install and can block redownload/recovery.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/src/services/FileSystem.ts` around
lines 285 - 298, The modelExists function currently returns true for any
non-empty folder; update modelExists to verify presence of framework-specific
artifact(s) instead of just folder non-emptiness: use the fw parameter to switch
on expected filenames/extensions (e.g., for 'ONNX' require a .onnx file, for
TensorFlow/TFLite require the specific .tflite/.pb or bundle filenames your app
expects, etc.), inspect the RNFS.readDir(folder) results for those expected
entries (by name or extension) and only return true if the required artifact(s)
are found; keep the early exists check but replace the generic files.length
check with the targeted artifact presence check inside modelExists.
🟡 Minor comments (11)
sdk/runanywhere-commons/src/utils/rac_image_utils.cpp-316-318 (1)

316-318: ⚠️ Potential issue | 🟡 Minor

Good validation, but inconsistent error code.

The channel validation correctly prevents out-of-bounds access to the mean/std arrays (size 3). However, other validation checks in this file use RAC_ERROR_INVALID_ARGUMENT (e.g., lines 251, 396, 426), while this uses RAC_ERROR_INVALID_PARAMETER.

Proposed fix for consistency
     if (image->channels < 1 || image->channels > 3) {
-        return RAC_ERROR_INVALID_PARAMETER;
+        return RAC_ERROR_INVALID_ARGUMENT;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/utils/rac_image_utils.cpp` around lines 316 -
318, The channel validation returns RAC_ERROR_INVALID_PARAMETER but other
validations in this file use RAC_ERROR_INVALID_ARGUMENT; update the check that
inspects image->channels (the if (image->channels < 1 || image->channels > 3)
branch) to return RAC_ERROR_INVALID_ARGUMENT instead of
RAC_ERROR_INVALID_PARAMETER so error codes are consistent across the file.
sdk/runanywhere-commons/src/infrastructure/network/api_types.cpp-533-539 (1)

533-539: ⚠️ Potential issue | 🟡 Minor

Add explicit include for SIZE_MAX definition.

The overflow check correctly guards against size_t wraparound: the logic event_count > (SIZE_MAX - kBaseEstimate) / kPerEventEstimate ensures the subsequent multiplication and addition won't overflow.

However, SIZE_MAX is not explicitly included. It's defined in <climits> or <cstdint>, neither of which appears in this file's includes. While some implementations transitively include it through <cstdlib>, this is not guaranteed by the C++ standard—particularly important given that this project explicitly uses gcc/g++ and notes that clang lacks proper C++ stdlib header support on the target platform. Per the Google C++ Style Guide followed by this project, add an explicit include:

`#include` <climits>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/network/api_types.cpp` around
lines 533 - 539, Add an explicit include for the header that defines SIZE_MAX by
inserting `#include` <climits> among the other includes at the top of
api_types.cpp so the overflow check that uses SIZE_MAX (around the calculation
with kPerEventEstimate, kBaseEstimate, and batch->event_count) is well-defined
across toolchains; ensure the new include is placed with the existing system
headers.
sdk/runanywhere-commons/src/features/llm/llm_component.cpp-706-712 (1)

706-712: ⚠️ Potential issue | 🟡 Minor

Emit a terminal failure event on this OOM return.

This branch returns before rac_lifecycle_track_error() and RAC_EVENT_LLM_GENERATION_FAILED, so a started stream can disappear from telemetry if the final strdup() fails.

Suggested patch
     final_result.text = strdup(ctx.full_text.c_str());
     if (!final_result.text) {
         log_error("LLM.Component", "Failed to allocate result text");
+        rac_lifecycle_track_error(component->lifecycle, RAC_ERROR_OUT_OF_MEMORY,
+                                  "generateStream");
+
+        rac_analytics_event_data_t event = {};
+        event.type = RAC_EVENT_LLM_GENERATION_FAILED;
+        event.data.llm_generation = RAC_ANALYTICS_LLM_GENERATION_DEFAULT;
+        event.data.llm_generation.generation_id = generation_id.c_str();
+        event.data.llm_generation.model_id = model_id;
+        event.data.llm_generation.model_name = model_name;
+        event.data.llm_generation.error_code = RAC_ERROR_OUT_OF_MEMORY;
+        event.data.llm_generation.error_message = "Failed to allocate result text";
+        rac_analytics_event_emit(RAC_EVENT_LLM_GENERATION_FAILED, &event);
+
         if (error_callback) {
             error_callback(RAC_ERROR_OUT_OF_MEMORY, "Failed to allocate result text", user_data);
         }
         return RAC_ERROR_OUT_OF_MEMORY;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/llm/llm_component.cpp` around lines 706
- 712, The OOM branch that returns when final_result.text is null should also
emit the terminal failure to telemetry: before returning from the allocation
check in the LLM component, call rac_lifecycle_track_error(...) with the
appropriate context and error code and emit RAC_EVENT_LLM_GENERATION_FAILED (via
the existing event tracking function used elsewhere) so the started stream is
marked failed; keep the existing log_error and error_callback but add the
rac_lifecycle_track_error(...) and the event emit (including any
message/user_data) immediately prior to returning RAC_ERROR_OUT_OF_MEMORY.
sdk/runanywhere-commons/src/features/stt/stt_component.cpp-342-345 (1)

342-345: ⚠️ Potential issue | 🟡 Minor

Use the resolved request sample_rate in telemetry.

local_options is the effective per-call config, but sample_rate is copied from component->config.sample_rate. If callers override options->sample_rate, the started/completed events will report the default instead of the value actually used for transcription.

🔧 Minimal fix
-        sample_rate = component->config.sample_rate;
-
         // Copy effective options to local so we can release the lock
         local_options = options ? *options : component->default_options;
+        sample_rate = local_options.sample_rate;

Also applies to: 382-385, 441-443

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` around lines 342
- 345, Telemetry uses component->config.sample_rate instead of the per-call
resolved value; after copying the effective per-call config into local_options
(from options ? *options : component->default_options), assign sample_rate =
local_options.sample_rate and use that for the started/completed events so
overridden options->sample_rate is reported; apply the same change in the other
occurrences where sample_rate is taken from component->config (the other blocks
around the start/completed event emission).
sdk/runanywhere-commons/src/features/stt/stt_component.cpp-16-16 (1)

16-16: ⚠️ Potential issue | 🟡 Minor

Add the missing header for snprintf.

Line 70 uses snprintf, but <cstdio> is not included. The code currently relies on transitive declarations, which is not portable. Add <cstdio> to the includes and preferably use std::snprintf for C++17 compliance.

Suggested fix
 `#include` <chrono>
+#include <cstdio>
 `#include` <cstdlib>
 `#include` <cstring>
 `#include` <mutex>
 `#include` <random>

Then at line 70:

-    snprintf(buffer, sizeof(buffer), "trans_%08x%08x", dis(gen), dis(gen));
+    std::snprintf(buffer, sizeof(buffer), "trans_%08x%08x", dis(gen), dis(gen));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` at line 16, Add
the missing C stdio header and prefer the C++-qualified function: include
<cstdio> alongside the existing includes (the file currently only shows `#include`
<random>) and replace or call the function as std::snprintf where snprintf is
used (the call around line 70) so the code no longer relies on transitive
declarations and is C++17-compliant.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt-211-212 (1)

211-212: ⚠️ Potential issue | 🟡 Minor

Add CppBridgeFileManager.unregister() and call it during shutdown for consistency.

CppBridgeFileManager.register() registers JNI file I/O callbacks, but there is no corresponding unregister() method or cleanup in shutdown(). Every other bridge extension follows a symmetric register/unregister lifecycle. Add an unregister() method to CppBridgeFileManager and call it in CppBridge.shutdown() to ensure proper cleanup on shutdown cycles and match the established pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt`
around lines 211 - 212, Add a symmetric cleanup path for the file manager:
implement CppBridgeFileManager.unregister() to undo whatever
CppBridgeFileManager.register() sets up (remove JNI callbacks/resources) and
invoke CppBridgeFileManager.unregister() from CppBridge.shutdown() (alongside
other bridge unregister calls) so the CppBridge.shutdown() lifecycle mirrors
registration and performs proper cleanup.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.kt-115-123 (1)

115-123: ⚠️ Potential issue | 🟡 Minor

Only treat an existing directory as success.

dir.exists() is also true for a regular file, so this reports success even when the target path is unusable as a directory. Check isDirectory before returning success.

Suggested fix
-                if (success || dir.exists()) 0 else -180 // RAC_ERROR_DIRECTORY_CREATION_FAILED
+                if (success || dir.isDirectory) 0 else -180 // RAC_ERROR_DIRECTORY_CREATION_FAILED
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.kt`
around lines 115 - 123, In createDirectory(path: String, recursive: Boolean)
update the success condition so an existing regular file isn't treated as a
successful directory creation: after attempting dir.mkdirs()/dir.mkdir() only
return success (0) if the operation succeeded or if dir.isDirectory() is true;
otherwise return the error code -180. Ensure the catch block still returns -180
and reference the createDirectory function and the local variable dir when
making this change.
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swift-149-153 (1)

149-153: ⚠️ Potential issue | 🟡 Minor

Don't hard-code fileCount to zero.

Every successful extraction now reports fileCount: 0, which makes ExtractionResult internally inconsistent and drops extraction stats for any downstream UI or analytics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swift`
around lines 149 - 153, The return currently hard-codes fileCount: 0 in the
ExtractionResult creating an incorrect metric; change it to return the actual
number of files produced by the extraction (e.g. use the count of the extracted
files collection or compute files under modelPath) so
ExtractionResult(modelPath: modelPath, extractedSize: extractedSize, fileCount:
<actualCount>, durationSeconds: duration) reflects the real value—look for the
extraction routine's list/array (e.g. extractedFiles, filesExtracted, or use
FileManager.contentsOfDirectory(atPath:) on modelPath) and substitute that count
for fileCount.
sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp-171-174 (1)

171-174: ⚠️ Potential issue | 🟡 Minor

This depth guard skips the last level.

With depth >= max_depth, a search starting at depth = 0 never inspects level max_depth itself. The current max_depth = 2 call only visits depths 0 and 1.

Suggested fix
-    if (depth >= max_depth) return false;
+    if (depth > max_depth) return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp`
around lines 171 - 174, The depth check in find_single_model_file incorrectly
stops when depth == max_depth so the final level is never searched; change the
guard from "if (depth >= max_depth) return false;" to "if (depth > max_depth)
return false;" (and ensure any recursive calls pass depth+1 so the function can
execute at depth == max_depth) so the function will inspect the max_depth level
as intended; reference: function find_single_model_file, parameters depth and
max_depth, and output buffer out_path.
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp-4306-4314 (1)

4306-4314: ⚠️ Potential issue | 🟡 Minor

Missing exception check after NewStringUTF.

If NewStringUTF fails (OOM), it returns nullptr and throws a pending exception. Calling JNI methods with a pending exception causes undefined behavior.

🛡️ Proposed fix
 static rac_result_t jni_fc_create_directory(const char* path, int recursive, void* user_data) {
     JNIEnv* env = getJNIEnv();
     if (env == nullptr || g_file_callbacks_obj == nullptr) return RAC_ERROR_NOT_INITIALIZED;
     jstring jPath = env->NewStringUTF(path);
+    if (jPath == nullptr) {
+        if (env->ExceptionCheck()) env->ExceptionClear();
+        return RAC_ERROR_OUT_OF_MEMORY;
+    }
     jint result = env->CallIntMethod(g_file_callbacks_obj, g_fc_create_directory, jPath,
                                      static_cast<jboolean>(recursive != 0));
     env->DeleteLocalRef(jPath);
     return static_cast<rac_result_t>(result);
 }

The same pattern should be applied to jni_fc_delete_path, jni_fc_list_directory, jni_fc_path_exists, and jni_fc_get_file_size.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
4306 - 4314, The NewStringUTF call in jni_fc_create_directory can return nullptr
and leave a pending exception (OOM); update jni_fc_create_directory to check if
jPath is nullptr or env->ExceptionCheck() is true after NewStringUTF, clean up
any local refs if needed, clear or leave the exception handled appropriately and
return RAC_ERROR_NOT_INITIALIZED (or an appropriate rac_result_t) instead of
calling CallIntMethod; apply the same null/exception-check pattern to
jni_fc_delete_path, jni_fc_list_directory, jni_fc_path_exists, and
jni_fc_get_file_size so none call JNI methods when NewStringUTF failed.
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp-4427-4454 (1)

4427-4454: ⚠️ Potential issue | 🟡 Minor

Missing verification that all method IDs were found.

If any GetMethodID call fails (method doesn't exist on the Kotlin side), it returns nullptr and sets a pending exception. The code should verify all method IDs before returning success.

🛡️ Proposed fix to verify method IDs
     g_fc_get_available_space = env->GetMethodID(cls, "getAvailableSpace", "()J");
     g_fc_get_total_space = env->GetMethodID(cls, "getTotalSpace", "()J");
     env->DeleteLocalRef(cls);

+    // Verify all required methods were found
+    if (!g_fc_create_directory || !g_fc_delete_path || !g_fc_list_directory ||
+        !g_fc_path_exists || !g_fc_is_directory || !g_fc_get_file_size ||
+        !g_fc_get_available_space || !g_fc_get_total_space) {
+        LOGe("File manager callbacks: missing required methods");
+        if (env->ExceptionCheck()) {
+            env->ExceptionDescribe();
+            env->ExceptionClear();
+        }
+        env->DeleteGlobalRef(g_file_callbacks_obj);
+        g_file_callbacks_obj = nullptr;
+        return RAC_ERROR_INVALID_ARGUMENT;
+    }
+
     LOGi("File manager callbacks registered");
     return RAC_SUCCESS;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
4427 - 4454, Verify that every cached jmethodID (g_fc_create_directory,
g_fc_delete_path, g_fc_list_directory, g_fc_path_exists, g_fc_is_directory,
g_fc_get_file_size, g_fc_get_available_space, g_fc_get_total_space) is non-null
after the GetMethodID calls in nativeFileManagerRegisterCallbacks; if any are
null, clean up by deleting the global ref g_file_callbacks_obj (and set it to
nullptr), delete the local class ref, check/clear the pending JNI exception via
env->ExceptionCheck()/env->ExceptionClear(), and return an appropriate error
code (e.g., RAC_ERROR_METHOD_NOT_FOUND) instead of RAC_SUCCESS. Ensure normal
flow returns RAC_SUCCESS only when all method IDs are valid.
🧹 Nitpick comments (9)
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swift (1)

121-124: Potential main-thread blocking and code duplication.

  1. @mainactor with I/O: This method calls the C++ bridge for file system checks while constrained to the main thread, which could cause UI hitches if the operation takes time.

  2. Duplication: This method is identical to modelFolderExists(modelId:framework:) at line 61. Consider having one call the other or extracting to a single implementation.

♻️ Suggested refactor
     `@MainActor`
     public func isModelDownloaded(modelId: String, framework: InferenceFramework) -> Bool {
-        return CppBridge.FileManager.modelFolderHasContents(modelId: modelId, framework: framework)
+        return modelFolderExists(modelId: modelId, framework: framework)
     }

Also consider whether @MainActor is necessary here, or if this could be made nonisolated or async to avoid blocking the main thread.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swift`
around lines 121 - 124, The isModelDownloaded(modelId:framework:) method
duplicates modelFolderExists(modelId:framework:) and is marked `@MainActor` while
calling a potentially blocking C++ file-system check; change isModelDownloaded
to reuse the existing implementation by delegating to
modelFolderExists(modelId:framework:) (or extract a shared private helper that
calls CppBridge.FileManager.modelFolderHasContents) and remove the `@MainActor`
isolation so the call won’t block the main thread—alternatively make it
async/nonisolated if the surrounding API expects concurrency; ensure any shared
helper has appropriate thread-safety if necessary.
sdk/runanywhere-commons/src/utils/rac_image_utils.cpp (1)

493-497: Reasonable defensive fix; consider logging a warning.

Clamping invalid dimensions to 1x1 prevents division by zero in the aspect ratio calculation below. However, silently fixing invalid input may mask bugs in upstream code. A debug log would help diagnose issues without changing behavior.

Optional: Add debug logging for invalid dimensions
     if (width <= 0 || height <= 0) {
+        RAC_LOG_DEBUG(LOG_CAT, "Invalid dimensions (%dx%d), clamping to 1x1", width, height);
         *out_width = 1;
         *out_height = 1;
         return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/utils/rac_image_utils.cpp` around lines 493 -
497, When detecting invalid input in the width/height check (the if (width <= 0
|| height <= 0) block that sets *out_width and *out_height to 1), add a
debug/warning log before clamping so upstream issues are visible; emit a concise
message that includes the offending width and height values and the
function/variable context (width, height, out_width, out_height) using the
project’s existing logger API (e.g., LOG_WARN/LOG_DEBUG or the module’s logger)
while keeping the current behavior of setting outputs to 1 and returning.
sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.h (1)

287-299: Don't publish an "internal helper" as RAC_API.

Line 298 exports rac_file_manager_clear_directory from the public header while the section/doc call it internal. That hardens a private helper into supported API surface for every SDK consumer. Either move it to a private header or document it as a stable public primitive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.h`
around lines 287 - 299, The declaration rac_file_manager_clear_directory is
marked with RAC_API in the public header but is described as an internal helper;
remove it from the public API by either (A) moving the declaration (and any
related types like rac_file_callbacks_t usage) into a private/internal header or
compilation unit and removing the RAC_API annotation from the public header, or
(B) if it must remain public, update the header comment and documentation to
declare rac_file_manager_clear_directory as a supported public primitive and
keep RAC_API; choose one approach and apply consistently to avoid exporting an
unintended private symbol.
sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart (1)

1473-1484: Document memory ownership for recommendation pointer.

RacStorageAvailabilityStruct.recommendation is a Pointer<Utf8> that callers must handle correctly. Consider adding a doc comment clarifying whether this string is owned by the struct (and freed with it) or must be freed separately by the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart`
around lines 1473 - 1484, RacStorageAvailabilityStruct.recommendation is a raw
Pointer<Utf8> with no documented ownership/lifetime — add a doc comment on the
recommendation field (and near the RacStorageAvailabilityStruct declaration)
that states whether the pointer is owned by the struct or by the caller, whether
the string is null-terminated, how long the pointer is valid (e.g. only until
the struct is freed or until an explicit free), and the exact method to release
it if the caller must free it (e.g. call freeString(), free via malloc.free, or
use a provided API function); reference
RacStorageAvailabilityStruct.recommendation in the comment so callers know to
follow the specified freeing convention.
sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp (1)

301-302: Good security practice clearing sensitive data from stack.

Zeroing the buffer after loading tokens helps prevent sensitive credential leakage via stack inspection. Note that aggressive compiler optimizations may elide this memset if the buffer is not subsequently used. For stronger guarantees, consider using explicit_bzero (POSIX) or SecureZeroMemory (Windows) if available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp` around
lines 301 - 302, The current memset(buffer, 0, sizeof(buffer)) can be optimized
away by compilers; replace this with a platform-appropriate secure zero routine
to ensure memory is wiped (e.g., call explicit_bzero(buffer, sizeof(buffer)) on
POSIX, SecureZeroMemory(buffer, sizeof(buffer)) on Windows, or a fallback to a
volatile memset wrapper if neither is available), keeping the same buffer
identifier so the sensitive token storage is cleared reliably; update any
`#includes` and add conditional compilation helpers (or a small secure_zero(void*,
size_t) wrapper) and use that in place of the existing memset call.
sdk/runanywhere-commons/tests/test_download_orchestrator.cpp (1)

42-50: Prefer std::filesystem over shelling out in these helpers.

system("rm -rf ...") / system("mkdir -p ...") makes the tests depend on /bin/sh and still leaves quoting edge cases. C++17 already gives you remove_all and create_directories in-process.

Possible cleanup

Add #include <filesystem> near the other headers, then:

 static void remove_dir(const std::string& path) {
-    std::string cmd = "rm -rf \"" + path + "\"";
-    system(cmd.c_str());
+    std::error_code ec;
+    std::filesystem::remove_all(path, ec);
 }

 static void mkdir_p(const std::string& path) {
-    std::string cmd = "mkdir -p \"" + path + "\"";
-    system(cmd.c_str());
+    std::error_code ec;
+    std::filesystem::create_directories(path, ec);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/tests/test_download_orchestrator.cpp` around lines 42
- 50, Replace shelling out in the helpers by using std::filesystem: include
<filesystem>, change remove_dir to call std::filesystem::remove_all(path) and
change mkdir_p to call std::filesystem::create_directories(path); ensure you
handle/propagate errors or ignore return values consistently (remove_all returns
count, create_directories returns bool) and remove the system(...) calls in
remove_dir and mkdir_p.
sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h (1)

82-135: Document the callback/lifetime contract in the public header.

This API documents parameters and error codes, but not whether rac_extraction_progress_fn is synchronous, which thread invokes it, or how long user_data and out_result must remain valid. That lifecycle contract belongs in the header for SDK integrators. As per coding guidelines, "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h`
around lines 82 - 135, The header is missing the lifecycle and threading
contract for rac_extraction_progress_fn and the pointer parameters; update the
comments for rac_extraction_progress_fn and rac_extract_archive_native to state
explicitly whether the callback is invoked synchronously or on a separate
thread, whether it may be re-entered or called concurrently, and the valid
lifetimes for user_data and out_result. Specifically: document that
rac_extraction_progress_fn is invoked by rac_extract_archive_native during the
extraction operation (synchronously on the extraction thread/caller thread),
that the callback will not be called after rac_extract_archive_native returns,
and that the caller must keep user_data and the out_result storage valid for the
entire duration of rac_extract_archive_native; also note thread-safety
expectations (e.g., callback may be called serially, not concurrently) and
mention behavior if async/streaming options change (point to
rac_extraction_options_t if it can enable async behavior). Include these
lifecycle sentences in the public header adjacent to the typedef for
rac_extraction_progress_fn and the rac_extract_archive_native declaration so
integrators know the contract.
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (1)

4294-4304: File callbacks globals lack thread-safety protection.

The global variables g_file_callbacks_obj and cached method IDs are accessed without mutex protection. If nativeFileManagerRegisterCallbacks is called while a file operation is in progress on another thread, this could cause a race condition.

Consider adding a mutex (similar to g_adapter_mutex used elsewhere in this file) if concurrent registration is possible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
4294 - 4304, The globals for file callbacks (g_file_callbacks_obj,
g_fc_create_directory, g_fc_delete_path, g_fc_list_directory, g_fc_path_exists,
g_fc_is_directory, g_fc_get_file_size, g_fc_get_available_space,
g_fc_get_total_space) need mutex protection to avoid races during registration
and use; add a dedicated mutex (or reuse g_adapter_mutex) and lock it around
writes in nativeFileManagerRegisterCallbacks and around reads/uses in any file
operation functions that access these globals, ensure proper JNI local/global
reference handling when updating g_file_callbacks_obj while holding the lock,
and release/clear the global safely when unregistering.
sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h (1)

38-115: Document thread-safety guarantees and callback lifecycle requirements.

Per coding guidelines, public C API headers in include/rac/ must document lifecycle requirements. The rac_file_callbacks_t struct should clarify:

  1. Whether callbacks must remain valid for the duration of any API call using them
  2. Thread-safety requirements (can the same callbacks struct be used from multiple threads?)
  3. Whether user_data ownership is transferred or borrowed

Consider adding a brief note, e.g.:

/**
 * `@note` Callbacks must remain valid for the duration of any rac_file_manager_* call.
 * `@note` Thread safety: Callers must synchronize concurrent access to shared user_data.
 */

Based on learnings: "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h`
around lines 38 - 115, Add lifecycle and thread-safety notes to the
rac_file_callbacks_t declaration: state that callbacks in rac_file_callbacks_t
must remain valid for the duration of any API call that receives the struct
(e.g., rac_file_manager_* functions), clarify that user_data is borrowed
(ownership not transferred) unless otherwise documented, and require callers to
synchronize concurrent access if the same rac_file_callbacks_t (or its
user_data) is used from multiple threads; reference the struct name
rac_file_callbacks_t and the user_data field in the note so readers can find the
relevant vtable and understand callback lifetime and thread-safety constraints.

Comment on lines +390 to +409
// Acquire lock only for state reads; release before long-running generation
rac_handle_t service = nullptr;
rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
if (result != RAC_SUCCESS) {
RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate");
return result;
}
rac_diffusion_options_t effective_options;
{
std::lock_guard<std::mutex> lock(component->mtx);

// Reset cancellation flag (also atomic, but set under lock for consistency)
component->cancel_requested = false;

// Get service from lifecycle manager
rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
if (result != RAC_SUCCESS) {
RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate");
return result;
}

// Merge user options over component defaults
rac_diffusion_options_t effective_options = merge_diffusion_options(
component->default_options, options);
// Merge user options over component defaults
effective_options = merge_diffusion_options(component->default_options, options);
}
// Lock released — safe to do long-running generation
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Keep the lifecycle service pinned for the full generation call.

After Line 408 and Line 506, load_model(), unload(), or cleanup() can acquire component->mtx and replace/reset the raw service handle while rac_diffusion_generate*() is still using it. The same change also makes concurrent generate*() calls share/reset one cancel_requested flag, so one request can cancel or revive another; either serialize generation again or add explicit service acquire/release semantics plus per-request cancellation state. As per coding guidelines, sdk/runanywhere-commons/src/**/*component*.{cpp,h}: Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes.

Also applies to: 418-419, 485-507, 524-527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp`
around lines 390 - 409, The code currently releases component->mtx before
calling rac_diffusion_generate*(), allowing load_model()/unload()/cleanup() to
replace the raw service handle or reset component->cancel_requested while
generation is in progress; fix by keeping the lifecycle service pinned for the
full generation call and using per-request cancellation state: call
rac_lifecycle_require_service (or the lifecycle pin API) to obtain/increment a
reference to the service handle (the same call used earlier) and store that
local pinned service, clear only a per-request cancel flag (not
component->cancel_requested) or allocate a request-local cancel token, then
perform rac_diffusion_generate*(pinned_service, ...) and finally call
rac_lifecycle_release_service(pinned_service) when generation completes; ensure
you remove reliance on component->cancel_requested inside the long-running
generate paths and update generate* functions to accept a per-request cancel
token so concurrent generate calls don’t interfere.

Comment on lines +351 to +352
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destDir.toNativeUtf8();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart | sed -n '340,380p'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1667


🌐 Web query:

Dart package:ffi String.toNativeUtf8() default allocator malloc calloc memory deallocation

💡 Result:

  • String.toNativeUtf8() (from package:ffi/ffi.dart) allocates a new, NUL-terminated UTF-8 buffer on the native heap and returns Pointer<Utf8>. Its default allocator is malloc: toNativeUtf8({Allocator allocator = malloc}). [1]
  • That memory is not GC-managed. Whoever allocates it must free it, using the corresponding allocator’s free() (the Allocator API is allocate(...) / free(pointer)). [2]

Practical rules

  1. If you call toNativeUtf8() and the C API only reads the string during the call, free it immediately after:

    import 'package:ffi/ffi.dart';
    
    final p = 'hello'.toNativeUtf8();     // default: malloc [1]
    try {
      nativeFunc(p);
    } finally {
      malloc.free(p);                     // or: allocator.free(p) [2]
    }
  2. If you pass allocator: calloc (e.g., you want the buffer zero-initialized beyond the written bytes), then free with that allocator:

    final p = 'hello'.toNativeUtf8(allocator: calloc);
    try { nativeFunc(p); } finally { calloc.free(p); }
  3. If the native side retains the pointer (stores it for later), you must define/know the ownership contract:

    • Either Dart keeps ownership and frees it later, or
    • The native library takes ownership and must provide a corresponding “free” API (recommended in general to avoid allocator-mismatch issues across libraries).

Notes

  • The ffi package’s own example demonstrates that the pointer returned by toNativeUtf8() must be freed after use. [3]

Sources: [1] StringUtf8Pointer.toNativeUtf8 API doc (default allocator = malloc), [2] dart:ffi Allocator API doc (allocate / free), [3] package:ffi example showing allocation + freeing of toNativeUtf8() result.


Fix allocator mismatch: toNativeUtf8() allocates with malloc but code frees with calloc.free().

String.toNativeUtf8() uses malloc by default. Freeing with calloc.free() is an allocator mismatch and corrupts native memory.

Use allocator: calloc to match the deallocator:

Fix
-    final archivePathPtr = archivePath.toNativeUtf8();
-    final destPathPtr = destDir.toNativeUtf8();
+    final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc);
+    final destPathPtr = destDir.toNativeUtf8(allocator: calloc);

Also applies to lines 369–370.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destDir.toNativeUtf8();
final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc);
final destPathPtr = destDir.toNativeUtf8(allocator: calloc);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 351 - 352, The native string pointers archivePathPtr and
destPathPtr (and the other two pointers on lines 369–370) are created with
String.toNativeUtf8() which uses malloc by default but are later freed with
calloc.free(), causing an allocator mismatch; update the allocations to use the
calloc allocator by calling toNativeUtf8(allocator: calloc) for archivePath,
destDir, and the other two strings so the subsequent calloc.free() calls match
the allocator.

Comment on lines +59 to +86
static rac_result_t posixDeletePath(const char* path, int recursive, void* /*userData*/) {
if (!path) return RAC_ERROR_NULL_POINTER;

struct stat st;
if (stat(path, &st) != 0) {
return RAC_SUCCESS; // Already gone
}

if (S_ISDIR(st.st_mode)) {
if (recursive) {
// Recursively delete directory contents
DIR* dir = opendir(path);
if (!dir) return RAC_ERROR_FILE_IO;

struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
std::string childPath = std::string(path) + "/" + entry->d_name;
posixDeletePath(childPath.c_str(), 1, nullptr);
}
closedir(dir);
}
return (rmdir(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO;
} else {
return (unlink(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't follow symlinks during recursive deletes.

posixDeletePath() uses stat(), so a symlink inside a managed directory is treated as its target. That lets clearCache(), clearTemp(), or deleteModel() recurse outside the SDK storage tree and delete arbitrary files. Given the new native archive flow, downloaded content can realistically surface symlinks here.

🔒 Safer symlink handling
     struct stat st;
-    if (stat(path, &st) != 0) {
+    if (lstat(path, &st) != 0) {
         return RAC_SUCCESS; // Already gone
     }
+
+    if (S_ISLNK(st.st_mode)) {
+        return (unlink(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 59 - 86, posixDeletePath currently uses stat which follows symlinks
and can recurse outside the intended tree; change the logic to use lstat when
checking the initial path and each child so symlinks are detected and not
followed. In the directory traversal inside posixDeletePath, for each entry
construct childPath, call lstat on childPath and if S_ISLNK skip it; if S_ISDIR
recurse into posixDeletePath, otherwise unlink regular files. Ensure you still
use rmdir/unlink return handling and avoid passing pointers to short-lived
strings into recursive calls by managing the childPath lifetime before calling
posixDeletePath.

@shubhammalhotra28
Copy link
Contributor Author

@coderabbitai please do a detailed review. Thanks!

@shubhammalhotra28
Copy link
Contributor Author

@greptile please do a detailed review. Thanks!

Comment on lines +574 to +677

// Download each file sequentially
bool any_failed = false;
for (size_t i = 0; i < file_count; ++i) {
const rac_model_file_descriptor_t& file = files[i];

// Build full download URL
std::string file_url = std::string(base_download_url);
if (!file_url.empty() && file_url.back() != '/') file_url += "/";
file_url += file.relative_path;

// Build destination path
std::string dest_path = std::string(model_folder);
if (file.destination_path && file.destination_path[0] != '\0') {
dest_path += "/" + std::string(file.destination_path);
} else {
dest_path += "/" + std::string(file.relative_path);
}

// Ensure parent directory exists
auto last_slash = dest_path.rfind('/');
if (last_slash != std::string::npos) {
mkdir_p(dest_path.substr(0, last_slash).c_str());
}

// For multi-file, download synchronously via platform adapter
// Update progress proportionally
double file_progress_start = static_cast<double>(i) / static_cast<double>(file_count);
double file_progress_end =
static_cast<double>(i + 1) / static_cast<double>(file_count);

// Update download manager with file-level progress
int64_t fake_downloaded = static_cast<int64_t>(file_progress_start * 100);
rac_download_manager_update_progress(dm_handle, task_id, fake_downloaded, 100);

// Start HTTP download for this file
// Note: Multi-file downloads are sequential — each file completes before next starts.
// For now we use the platform adapter's synchronous path.
char* http_task_id = nullptr;

// For multi-file we need a simpler blocking approach.
// Use shared_ptr to prevent double-free if callback fires unexpectedly.
struct multi_file_ctx {
bool completed;
rac_result_t result;
std::string downloaded_path;
};

auto file_ctx = std::make_shared<multi_file_ctx>(multi_file_ctx{false, RAC_SUCCESS, ""});

// Wrap in a raw holder for C callback void* — callback takes ownership of the holder
struct multi_file_holder {
std::shared_ptr<multi_file_ctx> ctx;
};
auto* file_holder = new multi_file_holder{file_ctx};

auto file_complete = [](rac_result_t result, const char* path, void* ud) {
auto* holder = static_cast<multi_file_holder*>(ud);
if (!holder || !holder->ctx) {
delete holder;
return;
}
holder->ctx->completed = true;
holder->ctx->result = result;
if (path) holder->ctx->downloaded_path = path;
delete holder;
};

rac_result_t http_result = rac_http_download(
file_url.c_str(), dest_path.c_str(), nullptr /* no per-file progress */, file_complete,
file_holder, &http_task_id);

if (http_task_id) rac_free(http_task_id);

if (http_result != RAC_SUCCESS) {
if (file.is_required == RAC_TRUE) {
any_failed = true;
RAC_LOG_ERROR(LOG_TAG, "Required file download failed to start: %s",
file.relative_path);
// Download never started, so callback won't fire — delete holder safely
delete file_holder;
break;
}
RAC_LOG_WARNING(LOG_TAG, "Optional file download failed to start: %s",
file.relative_path);
// Download never started, so callback won't fire — delete holder safely
delete file_holder;
continue;
}

// Download started — async callback will delete file_holder
}

if (any_failed) {
rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
"One or more required files failed to download");
} else {
// Update final progress
rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
}

*out_task_id = task_id;
return RAC_SUCCESS;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-file download marks task complete before transfers finish

rac_download_orchestrate_multi calls rac_http_download asynchronously (each call returns immediately with a task ID), but after the loop it calls rac_download_manager_mark_complete (or mark_failed) as if all transfers are done. In practice, the loop only verifies that each download started, not that it completed. The file_complete lambda sets file_ctx->completed = true and records the result, but nothing in the loop ever waits on that flag or checks file_ctx->result before advancing. As a result:

  1. The overall task is marked complete while files are still downloading in the background.
  2. Transfer-phase failures (network errors, partial data, etc.) inside the per-file callback are silently ignored — any_failed is only set when rac_http_download() itself returns an error at launch time, not when the transfer later fails.

The comment at line ~609 says "each file completes before next starts" and "synchronous path", but no synchronization primitive (semaphore, condition variable, or blocking join) is used to enforce this. The architecture as written is racing.

Consider introducing a synchronization mechanism (e.g., a semaphore or future/promise) to wait for each file's callback before proceeding to the next iteration, or restructure to chain completions properly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
Line: 574-677

Comment:
**Multi-file download marks task complete before transfers finish**

`rac_download_orchestrate_multi` calls `rac_http_download` asynchronously (each call returns immediately with a task ID), but after the loop it calls `rac_download_manager_mark_complete` (or `mark_failed`) as if all transfers are done. In practice, the loop only verifies that each download *started*, not that it *completed*. The `file_complete` lambda sets `file_ctx->completed = true` and records the result, but nothing in the loop ever waits on that flag or checks `file_ctx->result` before advancing. As a result:

1. The overall task is marked complete while files are still downloading in the background.
2. Transfer-phase failures (network errors, partial data, etc.) inside the per-file callback are silently ignored — `any_failed` is only set when `rac_http_download()` itself returns an error at launch time, not when the transfer later fails.

The comment at line ~609 says "each file completes before next starts" and "synchronous path", but no synchronization primitive (semaphore, condition variable, or blocking join) is used to enforce this. The architecture as written is racing.

Consider introducing a synchronization mechanism (e.g., a semaphore or future/promise) to wait for each file's callback before proceeding to the next iteration, or restructure to chain completions properly.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +667 to +677
if (any_failed) {
rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
"One or more required files failed to download");
} else {
// Update final progress
rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
}

*out_task_id = task_id;
return RAC_SUCCESS;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rac_download_orchestrate_multi returns RAC_SUCCESS on required-file failure

When a required file fails to start downloading (any_failed == true), the function still falls through to *out_task_id = task_id; return RAC_SUCCESS;. This means the caller receives a success code for an orchestration that already failed, making it impossible to distinguish a successful start from a failed one at the call site. The internal download manager state is correctly marked failed via rac_download_manager_mark_failed, but the function's own return value is misleading.

Suggested change
if (any_failed) {
rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
"One or more required files failed to download");
} else {
// Update final progress
rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
}
*out_task_id = task_id;
return RAC_SUCCESS;
if (any_failed) {
rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
"One or more required files failed to download");
rac_free(task_id);
return RAC_ERROR_DOWNLOAD_FAILED;
} else {
// Update final progress
rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
}
*out_task_id = task_id;
return RAC_SUCCESS;
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
Line: 667-677

Comment:
**`rac_download_orchestrate_multi` returns `RAC_SUCCESS` on required-file failure**

When a required file fails to start downloading (`any_failed == true`), the function still falls through to `*out_task_id = task_id; return RAC_SUCCESS;`. This means the caller receives a success code for an orchestration that already failed, making it impossible to distinguish a successful start from a failed one at the call site. The internal download manager state is correctly marked failed via `rac_download_manager_mark_failed`, but the function's own return value is misleading.

```suggestion
    if (any_failed) {
        rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
                                         "One or more required files failed to download");
        rac_free(task_id);
        return RAC_ERROR_DOWNLOAD_FAILED;
    } else {
        // Update final progress
        rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
        rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
    }

    *out_task_id = task_id;
    return RAC_SUCCESS;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +51 to +55
static const rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
RAC_TRUE, /* skip_macos_resources */
RAC_FALSE, /* skip_symlinks */
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static const struct default in header creates one copy per translation unit

RAC_EXTRACTION_OPTIONS_DEFAULT is declared static const in a header. Every .cpp file that includes this header will get its own private copy of the struct (that's the intent of static), which is harmless but wasteful. More importantly, in C++ with C++20 (per CLAUDE.md) this pattern should use inline constexpr to give the variable external linkage and a single definition, matching the project's C++20 requirement and avoiding potential size regressions when the header is included by many translation units.

Suggested change
static const rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
RAC_TRUE, /* skip_macos_resources */
RAC_FALSE, /* skip_symlinks */
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
};
inline constexpr rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
RAC_TRUE, /* skip_macos_resources */
RAC_FALSE, /* skip_symlinks */
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
};

Note: this is only valid in C++. Keep the static const form inside an #ifndef __cplusplus guard if C compatibility is needed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h
Line: 51-55

Comment:
**`static const` struct default in header creates one copy per translation unit**

`RAC_EXTRACTION_OPTIONS_DEFAULT` is declared `static const` in a header. Every `.cpp` file that includes this header will get its own private copy of the struct (that's the intent of `static`), which is harmless but wasteful. More importantly, in C++ with C++20 (per CLAUDE.md) this pattern should use `inline constexpr` to give the variable external linkage and a single definition, matching the project's C++20 requirement and avoiding potential size regressions when the header is included by many translation units.

```suggestion
inline constexpr rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
    RAC_TRUE,             /* skip_macos_resources */
    RAC_FALSE,            /* skip_symlinks */
    RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
};
```
Note: this is only valid in C++. Keep the `static const` form inside an `#ifndef __cplusplus` guard if C compatibility is needed.

How can I resolve this? If you propose a fix, please make it concise.

@shubhammalhotra28
Copy link
Contributor Author

@coderabbitai please do a detailed review. Thanks!

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

@shubhammalhotra28 Sure! I'll kick off a detailed review of PR #447 right away. This is a substantial PR covering:

  • C++ standard upgrade (C++17 → C++20) across all targets
  • libarchive integration replacing ZIPFoundation/SWCompression/Apache Commons Compress
  • New Download Orchestrator for centralized model download lifecycle management
  • New File Manager with cross-platform I/O callback bridges (Swift, Kotlin, Flutter, React Native)
  • New Extraction module using libarchive with streaming and security guards
  • Thread-safety & memory-safety improvements across backends (LlamaCPP, ONNX, WhisperCPP, VAD, etc.)
  • Unified RAC logging replacing platform-specific logging macros

I'll perform a thorough review across all changed files now.

✅ Actions performed

Full review triggered.

@shubhammalhotra28 shubhammalhotra28 changed the title Rag+cpp optis Cpp optis Mar 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (15)
sdk/runanywhere-react-native/packages/core/package.json (1)

49-66: ⚠️ Potential issue | 🟠 Major

Update documentation to reflect removal of react-native-zip-archive.

The package.json dependency cleanup is correct. However, the following documentation files still reference react-native-zip-archive and must be updated:

  • sdk/runanywhere-react-native/packages/core/README.md: Remove react-native-zip-archive from the installation instructions
  • sdk/runanywhere-react-native/Docs/ARCHITECTURE.md: Update to reflect that archive extraction is now handled by native C++ libarchive implementation instead of react-native-zip-archive

Stale documentation contradicts the dependency removal and will mislead users into installing an unnecessary package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/package.json` around lines 49 -
66, Update the two docs that still mention react-native-zip-archive: in
README.md remove the package from installation/installation instructions and any
example commands or yarn/npm entries, and in ARCHITECTURE.md replace references
to react-native-zip-archive with a short note that archive extraction is now
handled by the native C++ libarchive implementation (adjust any diagrams or text
describing the extraction flow accordingly and remove any platform-specific
setup steps for react-native-zip-archive).
sdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cpp (1)

12-12: 🛠️ Refactor suggestion | 🟠 Major

Update coding guidelines to reflect C++20 standard requirement.

The project CMakeLists files uniformly set CMAKE_CXX_STANDARD 20, confirming the C++20 upgrade is intentional across the codebase. However, the coding guidelines (CLAUDE.md) still specify "C++17 standard required" for sdk/runanywhere-commons/**/*.{cpp,hpp}. This inconsistency should be resolved by updating the documented standard to C++20 to maintain alignment between project documentation and actual build configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cpp`
at line 12, Update the coding guideline in CLAUDE.md that currently states
"C++17 standard required" for the sdk/runanywhere-commons/**/*.{cpp,hpp} pattern
to say "C++20 standard required" (to match project's CMAKE_CXX_STANDARD 20), and
while editing also scan for any other references to "C++17" (or legacy CXX
standard mentions) and update them to "C++20" so documentation and build config
are consistent.
sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp (2)

317-333: ⚠️ Potential issue | 🟠 Major

Inconsistent state if metadata file fails to open after index load.

If index_.load() succeeds (line 321) but the metadata file fails to open (lines 330-332), the method returns false without reverting index_. This leaves the store in an inconsistent state: index_ contains the newly loaded vectors while chunks_ and id_to_key_ retain stale data from before the load.

The fix on line 366 (index_.clear()) handles JSON parse failures but doesn't cover the file-open failure path. This was flagged in the PR description as a known issue.

Proposed fix: clear existing state before loading or revert on any failure
 bool load(const std::string& path) {
     std::lock_guard<std::mutex> lock(mutex_);
     
+    // Store original state for rollback on partial failure
+    auto original_index_size = index_.size();
+    
     // Load USearch index
     auto load_result = index_.load(path.c_str());
     if (!load_result) {
         RAC_LOG_ERROR(LOG_TAG,"Failed to load USearch index: %s", load_result.error.what());
         return false;
     }
     
     // Load metadata from JSON file
     std::string metadata_path = path + ".metadata.json";
     std::ifstream metadata_file(metadata_path);
     if (!metadata_file) {
         RAC_LOG_ERROR(LOG_TAG,"Failed to open metadata file: %s", metadata_path.c_str());
+        index_.clear(); // Revert index to consistent empty state
         return false;
     }

Alternatively, clear all state upfront before loading:

 bool load(const std::string& path) {
     std::lock_guard<std::mutex> lock(mutex_);
     
+    // Clear existing state before loading new data
+    index_.clear();
+    chunks_.clear();
+    id_to_key_.clear();
+    next_key_ = 0;
+    
     // Load USearch index
     auto load_result = index_.load(path.c_str());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp` around
lines 317 - 333, The load method can leave index_ populated while chunks_ and
id_to_key_ remain stale if the metadata file fails to open; either clear state
before attempting to load or revert on any failure: inside
VectorStoreUSEarch::load (protecting with mutex_), clear index_
(index_.clear()), chunks_, and id_to_key_ before calling index_.load(path) or,
if you prefer to load the index first, add a rollback path that calls
index_.clear() and clears chunks_ and id_to_key_ whenever opening the metadata
file or parsing JSON fails so the object never remains partially updated.

34-39: ⚠️ Potential issue | 🔴 Critical

Fix load() method state inconsistency and remove redundant macro definitions.

The load() method has a critical bug: if the metadata file fails to open after index_.load() succeeds (lines 330–332), the method returns without clearing the index. This leaves the index with new data while chunks_ and id_to_key_ retain old data, creating an inconsistent state. The index_.clear() at line 350 only executes in the catch block for JSON parse errors, not for file open failures.

Additionally, lines 37–39 define LOGI, LOGW, LOGE macros that are never used; the code consistently calls RAC_LOG_ERROR, RAC_LOG_INFO, and RAC_LOG_WARNING directly instead. Remove the unused macros.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp` around
lines 34 - 39, The load() method currently leaves index_ populated when
index_.load() succeeds but opening the metadata file fails; modify load() so
that immediately after a failed metadata file open (the block after
index_.load()), you call index_.clear() before returning and ensure chunks_ and
id_to_key_ remain consistent (clear or reset them as currently done in the JSON
parse catch path). Also remove the unused macro definitions LOGI/LOGW/LOGE (they
are redundant because the code uses RAC_LOG_INFO/RAC_LOG_WARNING/RAC_LOG_ERROR
directly) to clean up the header; keep references to index_.load(),
index_.clear(), chunks_, id_to_key_, and the load() method to locate the
changes.
sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp (2)

63-77: ⚠️ Potential issue | 🔴 Critical

Mirror the new input guard in transcribe_stream().

This path still calls convert_int16_to_float32() before validating audio_data and audio_size, so audio_data == nullptr with a non-zero size can still crash the process.

Suggested fix
 static rac_result_t whispercpp_stt_vtable_transcribe_stream(void* impl, const void* audio_data,
                                                             size_t audio_size,
                                                             const rac_stt_options_t* options,
                                                             rac_stt_stream_callback_t callback,
                                                             void* user_data) {
+    if (!audio_data || audio_size == 0 || (audio_size % sizeof(int16_t)) != 0) {
+        return RAC_ERROR_INVALID_ARGUMENT;
+    }
+
     // Fall back to batch transcription
     rac_stt_result_t result = {};
     std::vector<float> float_samples = convert_int16_to_float32(audio_data, audio_size);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp`
around lines 63 - 77, The stream path in whispercpp_stt_vtable_transcribe_stream
calls convert_int16_to_float32() before validating audio_data/audio_size and can
crash if audio_data == nullptr with non-zero size; mirror the input guard used
in transcribe_stream(): first check that audio_data is non-null and audio_size >
0 (and return the same error code used by transcribe_stream on invalid input),
only then call convert_int16_to_float32(), and proceed to call
rac_stt_whispercpp_transcribe and invoke the callback; ensure
rac_stt_result_free(&result) still runs on the normal path.

62-78: ⚠️ Potential issue | 🟠 Major

This is still a batch fallback, not streaming transcription.

Buffering the full clip, running one batch decode, and emitting a single final callback does not provide streaming STT behavior. Callers using the streaming vtable still won't get incremental partials or low-latency updates.

Based on learnings Applies to sdk/runanywhere-commons/src/backends/whispercpp/*.{cpp,h} : WhisperCPP backend must support GGML quantized models and implement streaming transcription.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp`
around lines 62 - 78, The current whispercpp_stt_vtable_transcribe_stream
implementation is only a batch fallback (buffers full clip, calls
rac_stt_whispercpp_transcribe once, emits single final callback); replace it
with a true streaming implementation that performs incremental chunked decoding
and emits partial callbacks: add/init streaming APIs (e.g.,
whispercpp_stt_stream_init, whispercpp_stt_stream_process_chunk,
whispercpp_stt_stream_finalize or reuse a whispercpp streaming decoder if
available), convert and feed audio in small chunks (update
convert_int16_to_float32 usage to support chunked conversion), call the callback
with partial transcripts (callback(text, RAC_FALSE, user_data)) during
processing and final (RAC_TRUE) on completion, and ensure GGML quantized model
loading/flags are used in the stream init so quantized models are supported;
finally update whispercpp_stt_vtable_transcribe_stream to delegate to the new
streaming flow and free stream state on completion.
sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp (1)

166-173: ⚠️ Potential issue | 🟠 Major

Rollback the tracker when outbound ID allocation fails.

Line 166 and Line 205 add the generation to active_generations before allocating out_generation_id. If malloc returns null, the call fails but the tracker remains orphaned in the map, and the caller never receives an ID that could later complete or fail that entry.

Suggested fix
     handle->active_generations[id] = tracker;

     // Allocate and copy the ID for the caller
     *out_generation_id = static_cast<char*>(malloc(id.size() + 1));
     if (!*out_generation_id) {
+        handle->active_generations.erase(id);
         return RAC_ERROR_OUT_OF_MEMORY;
     }
     memcpy(*out_generation_id, id.c_str(), id.size() + 1);

Also applies to: 205-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp` around lines 166
- 173, The code inserts tracker into handle->active_generations before
allocating *out_generation_id, which can orphan the tracker if malloc fails;
either allocate and copy the ID (the malloc/memcpy for out_generation_id) before
inserting tracker into handle->active_generations, or if you keep the current
order then ensure you erase/remove the tracker from handle->active_generations
on malloc failure (free any partially allocated resources and return
RAC_ERROR_OUT_OF_MEMORY); apply this change to both places where tracker is
inserted and out_generation_id is allocated (the two similar blocks handling
tracker/out_generation_id).
sdk/runanywhere-commons/src/features/llm/streaming_metrics.cpp (1)

1-10: ⚠️ Potential issue | 🟡 Minor

Update coding guidelines to reflect C++20 standard.

The actual C++ standard configured for sdk/runanywhere-commons is C++20 (set in CMakeLists.txt line 49), not C++17 as stated in the current coding guidelines. Update the guideline to C++20 standard required for consistency with the actual build configuration across all submodules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/llm/streaming_metrics.cpp` around lines
1 - 10, The repository's coding guidelines state C++17 but the project is built
with C++20 (see CMakeLists.txt at the project root referencing C++20); update
the guideline text to "C++20 standard required" wherever the coding standard is
documented (e.g., CONTRIBUTING, README, docs or any coding-guidelines files) so
it matches the actual build configuration used by sdk/runanywhere-commons and
all submodules; ensure references in comments or headers (for example the file
header in streaming_metrics.cpp) are also updated to reflect C++20.
sdk/runanywhere-commons/src/core/rac_audio_utils.cpp (1)

184-193: ⚠️ Potential issue | 🔴 Critical

Add a separate size_t overflow check before allocating.

The new RIFF guard keeps the header field valid, but wav_size = WAV_HEADER_SIZE + data_size can still wrap on 32-bit builds. If that happens, malloc() gets a tiny buffer and Line 205 copies pcm_size bytes into it.

🛠️ Suggested fix
+#include <limits>
+
     // Guard against WAV header overflow: the RIFF chunk-size field (data_size + 36)
     // is uint32_t, so data_size must leave room for the 36-byte header overhead.
     if (pcm_size > static_cast<size_t>(UINT32_MAX) - (WAV_HEADER_SIZE - 8)) {
         return RAC_ERROR_INVALID_ARGUMENT;
     }
+    if (pcm_size > std::numeric_limits<size_t>::max() - WAV_HEADER_SIZE) {
+        return RAC_ERROR_INVALID_ARGUMENT;
+    }
 
     const uint32_t data_size = static_cast<uint32_t>(pcm_size);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/rac_audio_utils.cpp` around lines 184 - 193,
The RIFF guard ensures the 32-bit header field fits but misses a platform-size
overflow for wav_size; before computing wav_size or allocating, add a size_t
overflow check such as verifying pcm_size <= SIZE_MAX - WAV_HEADER_SIZE (use
SIZE_MAX) and return RAC_ERROR_INVALID_ARGUMENT if it would overflow, then
compute wav_size = WAV_HEADER_SIZE + pcm_size (or WAV_HEADER_SIZE + data_size)
and proceed to malloc; reference symbols: pcm_size, WAV_HEADER_SIZE, data_size,
wav_size, and the allocation/copy path that follows.
sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp (1)

152-152: ⚠️ Potential issue | 🟠 Major

Require or preserve device_id during auth-state swaps.

rac_auth_build_refresh_request() hard-fails without g_auth_state.device_id, but this path frees the old state and then treats device_id as best-effort. If the response omits it or str_dup(response->device_id) fails, we still return success and mark the session authenticated, but the next refresh cannot be built.

🩹 Targeted fix
-    if (!response || !response->access_token || !response->refresh_token) {
+    if (!response || !response->access_token || !response->refresh_token ||
+        !response->device_id) {
         return -1;
     }

     // Pre-allocate required strings before modifying state
     char* new_access = str_dup(response->access_token);
     char* new_refresh = str_dup(response->refresh_token);
-    if (!new_access || !new_refresh) {
+    char* new_device = str_dup(response->device_id);
+    if (!new_access || !new_refresh || !new_device) {
         free(new_access);
         free(new_refresh);
+        free(new_device);
         return -1;
     }
@@
-    g_auth_state.device_id = str_dup(response->device_id);
+    g_auth_state.device_id = new_device;
     g_auth_state.user_id = str_dup(response->user_id);
     g_auth_state.organization_id = str_dup(response->organization_id);

Also applies to: 168-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp` at line
152, The code currently frees the old g_auth_state before swapping in the new
tokens and treats response->device_id as optional, which breaks
rac_auth_build_refresh_request() that requires device_id; change the swap logic
so you require or preserve device_id: when creating the new state (the branch
handling response->access_token/refresh_token), check response->device_id and
attempt to strdup it into the new state's device_id; if response->device_id is
NULL, copy the existing g_auth_state.device_id into the new state instead; if
strdup fails for the new device_id, abort the swap and keep the existing
g_auth_state (do not free it) and return an error; only mark the session
authenticated after a successful device_id assignment—this touches g_auth_state,
rac_auth_build_refresh_request(), response->device_id, and the code paths that
set authenticated.
sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (2)

689-711: ⚠️ Potential issue | 🟠 Major

Guard empty STT output here before invoking the LLM.

rac_voice_agent_process_voice_turn() already rejects !stt_result.text || strlen(stt_result.text) == 0, but this path forwards stt_result.text directly to rac_llm_component_generate(). A successful STT call with no text will now emit a transcription event and then hand a null/empty prompt to the LLM.

🐛 Minimal fix
     if (result != RAC_SUCCESS) {
         rac_voice_agent_event_t error_event = {};
         error_event.type = RAC_VOICE_AGENT_EVENT_ERROR;
         error_event.data.error_code = result;
         callback(&error_event, user_data);
         return result;
     }
+
+    if (!stt_result.text || strlen(stt_result.text) == 0) {
+        rac_stt_result_free(&stt_result);
+        rac_voice_agent_event_t error_event = {};
+        error_event.type = RAC_VOICE_AGENT_EVENT_ERROR;
+        error_event.data.error_code = RAC_ERROR_INVALID_STATE;
+        callback(&error_event, user_data);
+        return RAC_ERROR_INVALID_STATE;
+    }
 
     // Emit transcription event
     rac_voice_agent_event_t transcription_event = {};
     transcription_event.type = RAC_VOICE_AGENT_EVENT_TRANSCRIPTION;
     transcription_event.data.transcription = stt_result.text;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around
lines 689 - 711, After emitting the transcription_event, guard against empty STT
output before calling rac_llm_component_generate: check if stt_result.text is
NULL or strlen(stt_result.text) == 0 and if so emit an error (or return a
suitable rac_result_t) instead of invoking
rac_llm_component_generate(handle->llm_handle, stt_result.text, ...); update the
rac_voice_agent_process_voice_turn path to bail out when stt_result.text is
empty so the LLM is never called with a null/empty prompt (use rac_stt_result_t
stt_result, rac_voice_agent_event_t transcription_event,
rac_llm_component_generate and callback to locate where to add the check).

668-788: ⚠️ Potential issue | 🔴 Critical

Don't call callback(...) while holding handle->mutex.

Every callback in this function executes under the lock acquired at Line 669. A callback that re-enters rac_voice_agent_cleanup(), rac_voice_agent_destroy(), or another mutex-taking rac_voice_agent_* API will deadlock the calling thread, and slow callbacks now block load/cleanup/destroy as well. Build/copy the event payloads under lock, then release the mutex before dispatch.

As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around
lines 668 - 788, The function currently calls callback(...) while holding
handle->mutex which can deadlock; change the flow so you prepare and copy all
event payloads (transcription text from stt_result, response text from
llm_result, synthesized WAV bytes or a copied buffer) and any error codes while
holding the lock, then release the lock and invoke callback for transcription,
response, audio synthesized, and processed events; ensure you still call
validation helpers (validate_all_components_ready), component callers
(rac_stt_component_transcribe, rac_llm_component_generate,
rac_tts_component_synthesize, rac_audio_float32_to_wav) and cleanup helpers
(rac_stt_result_free, rac_llm_result_free, rac_tts_result_free) as before but
only perform memory allocations/copies under the mutex and move all
callback(...) invocations to after locking scope so callbacks run without
holding handle->mutex.
sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp (2)

118-153: ⚠️ Potential issue | 🟠 Major

Initialize out_result before the new OOM exits.

The added RAC_ERROR_OUT_OF_MEMORY returns at Lines 140-148 happen before words, num_words, and the timing fields are assigned. That leaves the result struct partially uninitialized on failure, which is risky if callers always pass it through a cleanup/helper path.

🔧 Proposed fix
 rac_result_t rac_stt_onnx_transcribe(rac_handle_t handle, const float* audio_samples,
                                      size_t num_samples, const rac_stt_options_t* options,
                                      rac_stt_result_t* out_result) {
     if (handle == nullptr || audio_samples == nullptr || out_result == nullptr) {
         return RAC_ERROR_NULL_POINTER;
     }
+
+    out_result->text = nullptr;
+    out_result->detected_language = nullptr;
+    out_result->words = nullptr;
+    out_result->num_words = 0;
+    out_result->confidence = 0.0f;
+    out_result->processing_time_ms = 0.0;
 
     auto* h = static_cast<rac_onnx_stt_handle_impl*>(handle);
     if (!h->stt) {
         return RAC_ERROR_INVALID_HANDLE;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp` around lines 118 -
153, In rac_stt_onnx_transcribe, ensure the out_result struct is fully
initialized before any early returns for OOM: set out_result->words = nullptr,
out_result->num_words = 0, out_result->confidence = 1.0f,
out_result->processing_time_ms = 0 (or a safe default) immediately after the
null/handle checks and before allocating/strdup calls so that any return (e.g.,
the RAC_ERROR_OUT_OF_MEMORY paths around strdup for out_result->text and
out_result->detected_language) leaves out_result in a consistent, initialized
state; adjust the logic in rac_stt_onnx_transcribe accordingly (initialize
out_result early, then perform strdup and handle freeing on failure).

218-230: ⚠️ Potential issue | 🟡 Minor

Return nullptr for empty streaming transcripts too.

rac_stt_onnx_transcribe() maps empty text to nullptr, but rac_stt_onnx_decode_stream() currently allocates "". That gives batch and streaming callers different empty-result semantics for the same API surface.

🔧 Proposed fix
     auto result = h->stt->decode(stream_id);
+    if (result.text.empty()) {
+        *out_text = nullptr;
+        return RAC_SUCCESS;
+    }
     *out_text = strdup(result.text.c_str());
     if (!*out_text) {
         return RAC_ERROR_OUT_OF_MEMORY;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp` around lines 218 -
230, The function rac_stt_onnx_decode_stream currently strdup's an empty string
which differs from rac_stt_onnx_transcribe's behavior; update
rac_stt_onnx_decode_stream (and the local variables around h->stt->decode) to
check if result.text.empty() and, if so, set *out_text = nullptr and return
success, otherwise strdup result.text.c_str() and still check for allocation
failure (RAC_ERROR_OUT_OF_MEMORY); ensure no memory is allocated for the empty
case and maintain existing null-pointer checks at the top of the function.
sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)

477-509: ⚠️ Potential issue | 🟠 Major

Track lifecycle state explicitly instead of deriving it from is_initialized.

rac_vad_component_get_state() can only report IDLE or LOADED, and rac_vad_component_get_metrics() fabricates load counts from that same boolean. start(), stop(), reset(), and cleanup transitions are never recorded, so callers cannot rely on this lifecycle API.

Based on learnings "Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 477
- 509, The component currently derives lifecycle from is_initialized causing
rac_vad_component_get_state and rac_vad_component_get_metrics to be inaccurate;
add an explicit lifecycle field (e.g., rac_lifecycle_state_t lifecycle_state)
and counters (total_loads, successful_loads, failed_loads) to the
rac_vad_component struct, update start(), stop(), reset(), and cleanup functions
to set lifecycle_state on each transition and increment the appropriate
counters, then change rac_vad_component_get_state() to return
component->lifecycle_state and rac_vad_component_get_metrics() to copy the real
counters into out_metrics while holding component->mtx and validating arguments.
♻️ Duplicate comments (13)
sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (1)

240-261: ⚠️ Potential issue | 🔴 Critical

rac_voice_agent_destroy() still tears down the handle while callers can be live.

The mutex only protects threads that are already inside the critical section. Threads still queued on handle->mutex can wake after the lock drops, and rac_voice_agent_detect_speech() still bypasses that mutex entirely, so Line 261 can free the handle while other APIs still dereference it. Add a real shutdown barrier/in-flight counter and wait for it to drain before deleting the object.

As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around
lines 240 - 261, rac_voice_agent_destroy() currently releases handle->mutex and
immediately deletes handle while callers (e.g., rac_voice_agent_detect_speech)
may still access it; add a proper shutdown barrier by introducing an in-flight
counter and a shutdown flag on the handle (e.g., atomic<int> inflight_count and
atomic<bool> shutting_down), incrementing/decrementing inflight_count in
entry/exit paths of public APIs (including rac_voice_agent_detect_speech) and
making rac_voice_agent_destroy() set shutting_down/is_configured=false, wait
(spin or condition_variable) until inflight_count == 0, then proceed to destroy
owned components and delete handle; update rac_voice_agent_detect_speech and
other callers to check shutting_down/is_configured and return early while still
correctly decrementing the counter so destroy can safely delete the handle.
sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp (1)

189-192: ⚠️ Potential issue | 🔴 Critical

Advance pos before continuing on allocation failure.

If either strdup fails, the loop continues without advancing pos, causing an infinite retry of the same object. This turns an OOM condition into a hang.

🐛 Proposed fix
         if (!model->id || !model->name) {
             rac_model_info_free(model);
+            pos = obj_end;
             continue;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp`
around lines 189 - 192, The loop over model entries is not advancing pos on
allocation failure, causing an infinite retry; in the block that checks if
(!model->id || !model->name) (after strdup failures), free the model
(rac_model_info_free(model)) and then increment the loop index variable pos (or
the iterator used to traverse entries) before continuing so the code advances to
the next item instead of retrying the same one.
sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp (1)

74-79: ⚠️ Potential issue | 🔴 Critical

Constructor exceptions can still cross the extern "C" boundary here.

new (std::nothrow) only converts allocation failure into nullptr; it does not swallow exceptions thrown while constructing rac_vad_analytics_s. If that constructor throws, this function still terminates the process instead of returning a rac_result_t.

In C++, does `new (std::nothrow) T(...)` suppress exceptions thrown by `T`'s constructor, or only allocation failures?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp` around lines 74 -
79, The allocation can still throw from the constructor of rac_vad_analytics_s;
wrap the new (std::nothrow) rac_vad_analytics_s() call in a try/catch inside the
function (the one that sets *out_handle) so constructor exceptions are caught,
log the exception message (use exception.what()) and return a non-success
rac_result_t instead of letting the exception cross the extern "C" boundary;
also catch (...) for non-std::exception cases and return an appropriate error
code. Ensure you still handle the nullptr case (allocation failure) as before,
and reference the allocation and type name rac_vad_analytics_s and the output
pointer *out_handle in your changes.
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (1)

4207-4239: ⚠️ Potential issue | 🔴 Critical

Rename these JNI exports to native_bridge, not native_1bridge.

These symbols currently map to com.runanywhere.sdk.native_bridge.RunAnywhereBridge, while the bridge class in this PR lives under com.runanywhere.sdk.native.bridge.RunAnywhereBridge. The JVM will not find these methods, so every new archive/file-manager call here fails with UnsatisfiedLinkError at runtime.

#!/bin/bash
echo "=== New JNI exports ==="
rg -n "Java_com_runanywhere_sdk_native(_1)?bridge_RunAnywhereBridge_(nativeExtractArchive|nativeDetectArchiveType|nativeFindModelPathAfterExtraction|nativeDownloadRequiresExtraction|nativeComputeDownloadDestination|nativeFileManager)" sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp

echo
echo "=== Kotlin package declaration ==="
fd "RunAnywhereBridge.kt" sdk/runanywhere-kotlin -x sed -n '1,40p' {}

Also applies to: 4245-4582

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
4207 - 4239, The JNI exported function names use "native_1bridge" but the
Java/Kotlin class lives in package com.runanywhere.sdk.native.bridge, so rename
the JNI symbols to use "native_bridge" (remove the "_1" escape) to match the
JVM-mangled names; specifically update
Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeExtractArchive
and
Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeDetectArchiveType
(and the other related exports
Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeFindModelPathAfterExtraction,
_nativeDownloadRequiresExtraction, _nativeComputeDownloadDestination,
_nativeFileManager) to
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_<methodName> so the
signatures match com.runanywhere.sdk.native.bridge.RunAnywhereBridge and the JVM
can bind them. Ensure each renamed JNI function keeps the same parameters and
return types and update any matching forward-declarations or references in the
same file.
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart (1)

8-8: ⚠️ Potential issue | 🔴 Critical

Add the missing dart:io import.

_ensureDirectoryExists() still instantiates Directory(path), so this file will not compile until dart:io is imported.

Minimal fix
 import 'dart:ffi';
+import 'dart:io';

Run this to confirm the unresolved import:

#!/bin/bash
sed -n '1,20p' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart
echo "---"
rg -n '\bDirectory\s*\(' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart
echo "---"
rg -n "^import 'dart:io';" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart || true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`
at line 8, The file is missing the dart:io import causing Directory usage in
_ensureDirectoryExists() to fail; add import 'dart:io'; alongside the existing
import 'package:runanywhere/native/dart_bridge_download.dart' so Directory(path)
and related types resolve and the file will compile.
sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)

184-185: ⚠️ Potential issue | 🔴 Critical

These getter locks still deadlock on callback re-entry.

rac_vad_component_process(), rac_vad_component_stop(), and rac_vad_component_cleanup() still hold component->mtx while calling rac_energy_vad_*. That path can synchronously reach vad_speech_activity_callback() and user code, so any callback that queries init/state/metrics will re-enter these getters and block on the same mutex. Use atomics/snapshots for the read APIs, or release component->mtx before calling into the VAD engine.

Also applies to: 482-503

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 184
- 185, The getters (eg. rac_vad_component_is_initialized) deadlock because
component->mtx is held while code paths in rac_vad_component_process,
rac_vad_component_stop, and rac_vad_component_cleanup call into rac_energy_vad_*
which can synchronously re-enter vad_speech_activity_callback and user code that
calls those getters; fix by removing long-held mutex usage: either make the
state fields atomic or take a local snapshot of the fields under the lock and
return the snapshot (no lock held during callbacks), and/or ensure
rac_vad_component_process/stop/cleanup release component->mtx before calling
rac_energy_vad_* so callbacks can safely call the getters without blocking.
Target the functions rac_vad_component_process, rac_vad_component_stop,
rac_vad_component_cleanup and the getter (is_initialized) to apply the
atomic/snapshot change or to move the rac_energy_vad_* call outside the locked
section.
sdk/runanywhere-commons/src/server/http_server.cpp (1)

57-111: ⚠️ Potential issue | 🔴 Critical

Catch exceptions before returning through rac_server_start().

std::filesystem::exists, std::make_unique, and std::thread can still throw in this startup path, and rac_server_start() forwards HttpServer::start() straight through the C API. Convert those failures to a rac_result_t instead of letting exceptions escape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 57 - 111,
Wrap the body of HttpServer::start (after acquiring the mutex_) in a try/catch
that catches std::exception and ...; on exception return an appropriate
rac_result_t (e.g., RAC_ERROR_INTERNAL) and log the exception message via
RAC_LOG_ERROR; ensure any partially initialized members (server_, serverThread_,
loaded model state) are cleaned/reset before returning—target the section that
calls std::filesystem::exists, loadModel(modelPath_),
std::make_unique<httplib::Server>(), setupCors(), setupRoutes(), and
serverThread_ (std::thread(&HttpServer::serverThread, this)) so no C++ exception
propagates out of HttpServer::start and all errors convert to rac_result_t.
sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp (1)

59-86: ⚠️ Potential issue | 🔴 Critical

Don't follow symlinks during recursive deletes.

stat() resolves the target before the type check here, so a symlink inside cache/temp/model storage can make clearCache(), clearTemp(), or deleteModel() recurse outside the managed tree. Use lstat() and unlink symlinks instead of descending into them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 59 - 86, The posixDeletePath function currently uses stat which
follows symlinks and can recursively escape the managed tree; switch to lstat in
posixDeletePath to detect S_ISLNK and treat symlinks as files (call unlink on
the path and return appropriate RAC_SUCCESS/RAC_ERROR_FILE_IO) instead of
descending into them; additionally, when iterating directory entries in
posixDeletePath, call lstat on each childPath (not stat) and if S_ISLNK unlink
the child, if S_ISDIR recurse (with the same lstat-based checks), otherwise
unlink the child; ensure DIR* is closed on all paths and propagate error codes
from unlink/rmdir calls consistently.
sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp (1)

37-55: ⚠️ Potential issue | 🔴 Critical

Backslash and Windows paths still bypass the zip-slip checks.

is_path_safe() only blocks /-rooted and /../ forms, and hardlink targets are rewritten without the same validation. Entries like ..\payload, C:\payload, or \\server\share can still escape destination_dir on Windows or via link targets.

Also applies to: 217-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`
around lines 37 - 55, is_path_safe currently only checks POSIX-style absolute
and ".." components and misses Windows-style backslashes, drive letters and UNC
paths, and hardlink target validation; update is_path_safe to normalize or
reject backslashes by treating '\' as a path separator, reject Windows absolute
forms (leading drive letter like "C:" or leading "\\" for UNC), normalize
separators and collapse/sanitize "." and ".." components before acceptance, and
apply the same validation to any hardlink target rewriting logic (the
function/logic that rewrites hardlink targets mentioned around lines 217-245).
Ensure you reference and modify is_path_safe and the hardlink-target rewrite
code so both use the same normalized-path validation routine.
sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (2)

364-367: ⚠️ Potential issue | 🔴 Critical

Throw on native extraction failure instead of returning the archive path.

Returning archivePath keeps the caller on the success path: it deletes the archive, updates the registry, and emits completion for an unextracted model.

Fix
       if (result != 0) {
-        _logger.error('Native extraction failed with code: $result');
-        return archivePath;
+        throw Exception(
+          'Native extraction failed with code: $result for $archivePath',
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 364 - 367, The code currently logs an extraction error and returns
archivePath which makes the caller proceed as if extraction succeeded; instead,
in the extraction routine where you check "if (result != 0)" (using variables
result, archivePath and logger _logger.error), throw a descriptive exception
(including result and archivePath) rather than returning archivePath so the
caller will treat it as a failure and will not delete the archive or update the
registry/emit completion for an unextracted model.

351-352: ⚠️ Potential issue | 🔴 Critical

Match toNativeUtf8() with the allocator you free.

toNativeUtf8() uses malloc by default, so freeing these pointers with calloc.free() is an allocator mismatch and can corrupt native memory.

Fix
-    final archivePathPtr = archivePath.toNativeUtf8();
-    final destPathPtr = destDir.toNativeUtf8();
+    final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc);
+    final destPathPtr = destDir.toNativeUtf8(allocator: calloc);

Also applies to: 369-370

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 351 - 352, toNativeUtf8() defaults to malloc, but the code frees
the returned pointers with calloc.free(), causing an allocator mismatch; for
archivePathPtr and destPathPtr (and the other pointers at the same pattern
around the later block) either pass allocator: calloc to toNativeUtf8(…) or
change the frees to use malloc.free(…) so allocator used to allocate matches the
one used to free; update the calls around archivePathPtr and destPathPtr (and
the matching pointers at the later occurrence) accordingly and ensure the proper
allocator import is available.
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dart (1)

148-158: ⚠️ Potential issue | 🔴 Critical

Match the allocator used by toNativeUtf8().

These helpers allocate UTF-8 inputs with malloc (the default for toNativeUtf8()), but free them with calloc.free(). That is allocator-mismatched native memory corruption on every call path.

Fix
-      final dirPtr = extractedDir.toNativeUtf8();
+      final dirPtr = extractedDir.toNativeUtf8(allocator: calloc);
...
-      final urlPtr = url.toNativeUtf8();
+      final urlPtr = url.toNativeUtf8(allocator: calloc);
...
-      final modelIdPtr = modelId.toNativeUtf8();
-      final urlPtr = downloadUrl.toNativeUtf8();
+      final modelIdPtr = modelId.toNativeUtf8(allocator: calloc);
+      final urlPtr = downloadUrl.toNativeUtf8(allocator: calloc);

Also applies to: 176-181, 207-224

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dart`
around lines 148 - 158, The UTF-8 pointer from extractedDir.toNativeUtf8() is
allocated with malloc but is being freed with calloc.free(), causing allocator
mismatch; fix by matching allocators consistently—either allocate dirPtr with
the same allocator as outPath (call extractedDir.toNativeUtf8(allocator:
calloc)) or free dirPtr with malloc.free() instead of calloc.free(); apply the
same fix to the other occurrences referenced (around the blocks for lines
~176-181 and ~207-224) so every toNativeUtf8() allocation uses the same
allocator as its corresponding free.
sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp (1)

575-678: ⚠️ Potential issue | 🔴 Critical

Race condition in rac_download_orchestrate_multi() — downloads complete prematurely.

This issue was flagged in a previous review and remains unresolved. The code comments claim "sequential — each file completes before next starts" (lines 610-611), but rac_http_download is asynchronous and returns immediately after starting the download.

The current flow:

  1. Loop starts HTTP download for file i
  2. rac_http_download returns RAC_SUCCESS (download started, not completed)
  3. Loop immediately proceeds to file i+1
  4. After all downloads are started, lines 667-674 mark the task complete

This means rac_download_manager_mark_complete is called while downloads are still in flight, and failures in required files won't be detected.

To fix: After rac_http_download returns success, poll/wait for file_ctx->completed before proceeding to the next file, then check file_ctx->result to detect failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp`
around lines 575 - 678, After starting each async download with
rac_http_download (in rac_download_orchestrate_multi), wait for that file's
completion by polling the shared file_ctx->completed flag (the shared_ptr
created as file_ctx) before continuing to the next iteration; once completed
check file_ctx->result and if it is not RAC_SUCCESS treat required files
(file.is_required) as failures (set any_failed, log via
RAC_LOG_ERROR/RAC_LOG_WARNING) and break/continue accordingly, and only call
rac_download_manager_mark_complete or rac_download_manager_mark_failed after all
file results are observed; do not delete file_holder after a successful start
(the file_complete callback owns and deletes it), and update per-file progress
via rac_download_manager_update_progress based on
file_progress_start/file_progress_end before/after the wait.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14669245-45cb-41f7-8936-f210b100a466

📥 Commits

Reviewing files that changed from the base of the PR and between 596e8c6 and 67b4c22.

⛔ Files ignored due to path filters (3)
  • examples/react-native/RunAnywhereAI/package-lock.json is excluded by !**/package-lock.json
  • examples/react-native/RunAnywhereAI/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • sdk/runanywhere-react-native/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (141)
  • Package.resolved
  • Package.swift
  • Playground/README.md
  • Playground/linux-voice-assistant/CMakeLists.txt
  • Playground/linux-voice-assistant/README.md
  • Playground/openclaw-hybrid-assistant/CMakeLists.txt
  • examples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
  • examples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.m
  • examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • sdk/runanywhere-commons/.clang-format
  • sdk/runanywhere-commons/CLAUDE.md
  • sdk/runanywhere-commons/CMakeLists.txt
  • sdk/runanywhere-commons/README.md
  • sdk/runanywhere-commons/VERSIONS
  • sdk/runanywhere-commons/exports/RACommons.exports
  • sdk/runanywhere-commons/include/rac/core/rac_logger.h
  • sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download.h
  • sdk/runanywhere-commons/include/rac/infrastructure/download/rac_download_orchestrator.h
  • sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h
  • sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h
  • sdk/runanywhere-commons/src/backends/llamacpp/CMakeLists.txt
  • sdk/runanywhere-commons/src/backends/llamacpp/jni/rac_backend_llamacpp_jni.cpp
  • sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp
  • sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h
  • sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp
  • sdk/runanywhere-commons/src/backends/llamacpp/rac_vlm_llamacpp.cpp
  • sdk/runanywhere-commons/src/backends/onnx/CMakeLists.txt
  • sdk/runanywhere-commons/src/backends/onnx/jni/rac_backend_onnx_jni.cpp
  • sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp
  • sdk/runanywhere-commons/src/backends/onnx/onnx_backend.h
  • sdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cpp
  • sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp
  • sdk/runanywhere-commons/src/backends/whispercpp/CMakeLists.txt
  • sdk/runanywhere-commons/src/backends/whispercpp/jni/rac_backend_whispercpp_jni.cpp
  • sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp
  • sdk/runanywhere-commons/src/backends/whispercpp/rac_stt_whispercpp.cpp
  • sdk/runanywhere-commons/src/backends/whispercpp/whispercpp_backend.cpp
  • sdk/runanywhere-commons/src/backends/whisperkit_coreml/CMakeLists.txt
  • sdk/runanywhere-commons/src/backends/whisperkit_coreml/rac_backend_whisperkit_coreml_register.cpp
  • sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp
  • sdk/runanywhere-commons/src/core/rac_audio_utils.cpp
  • sdk/runanywhere-commons/src/core/rac_core.cpp
  • sdk/runanywhere-commons/src/core/rac_error.cpp
  • sdk/runanywhere-commons/src/core/rac_logger.cpp
  • sdk/runanywhere-commons/src/core/rac_structured_error.cpp
  • sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp
  • sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp
  • sdk/runanywhere-commons/src/features/llm/llm_component.cpp
  • sdk/runanywhere-commons/src/features/llm/streaming_metrics.cpp
  • sdk/runanywhere-commons/src/features/llm/tool_calling.cpp
  • sdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cpp
  • sdk/runanywhere-commons/src/features/rag/CMakeLists.txt
  • sdk/runanywhere-commons/src/features/rag/jni/rac_rag_jni.cpp
  • sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp
  • sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp
  • sdk/runanywhere-commons/src/features/stt/stt_analytics.cpp
  • sdk/runanywhere-commons/src/features/stt/stt_component.cpp
  • sdk/runanywhere-commons/src/features/tts/tts_analytics.cpp
  • sdk/runanywhere-commons/src/features/tts/tts_component.cpp
  • sdk/runanywhere-commons/src/features/vad/energy_vad.cpp
  • sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp
  • sdk/runanywhere-commons/src/features/vad/vad_component.cpp
  • sdk/runanywhere-commons/src/features/vlm/vlm_component.cpp
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp
  • sdk/runanywhere-commons/src/features/wakeword/wakeword_service.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_manager.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
  • sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp
  • sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp
  • sdk/runanywhere-commons/src/infrastructure/file_management/file_manager.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_paths.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_registry.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_strategy.cpp
  • sdk/runanywhere-commons/src/infrastructure/network/api_types.cpp
  • sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp
  • sdk/runanywhere-commons/src/infrastructure/network/http_client.cpp
  • sdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cpp
  • sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp
  • sdk/runanywhere-commons/src/jni/CMakeLists.txt
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-commons/src/server/CMakeLists.txt
  • sdk/runanywhere-commons/src/server/http_server.cpp
  • sdk/runanywhere-commons/src/server/http_server.h
  • sdk/runanywhere-commons/src/server/openai_handler.cpp
  • sdk/runanywhere-commons/src/utils/rac_image_utils.cpp
  • sdk/runanywhere-commons/tests/CMakeLists.txt
  • sdk/runanywhere-commons/tests/test_download_orchestrator.cpp
  • sdk/runanywhere-commons/tests/test_extraction.cpp
  • sdk/runanywhere-commons/tools/CMakeLists.txt
  • sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/file_management/services/simplified_file_manager.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
  • sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
  • sdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx_download_strategy.dart
  • sdk/runanywhere-flutter/packages/runanywhere_onnx/pubspec.yaml
  • sdk/runanywhere-kotlin/build.gradle.kts
  • sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/build.gradle.kts
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeModelRegistry.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.jvmAndroid.kt
  • sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+Storage.jvmAndroid.kt
  • sdk/runanywhere-react-native/packages/core/android/CMakeLists.txt
  • sdk/runanywhere-react-native/packages/core/android/build.gradle
  • sdk/runanywhere-react-native/packages/core/android/consumer-rules.pro
  • sdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cpp
  • sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/ArchiveUtility.kt
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.hpp
  • sdk/runanywhere-react-native/packages/core/ios/ArchiveUtility.swift
  • sdk/runanywhere-react-native/packages/core/ios/ArchiveUtilityBridge.m
  • sdk/runanywhere-react-native/packages/core/package.json
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/RunAnywhere+Storage.ts
  • sdk/runanywhere-react-native/packages/core/src/services/FileSystem.ts
  • sdk/runanywhere-react-native/packages/core/src/services/index.ts
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_download_orchestrator.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Download.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+FileManager.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+ModelRegistry.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Storage.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService+Execution.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Utilities/ArchiveUtility.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Utilities/FileOperationsUtilities.swift
  • sdk/runanywhere-web/packages/core/src/Infrastructure/ArchiveUtility.ts
  • sdk/runanywhere-web/packages/llamacpp/src/Foundation/PlatformAdapter.ts
  • sdk/runanywhere-web/wasm/CMakeLists.txt
💤 Files with no reviewable changes (12)
  • sdk/runanywhere-react-native/packages/core/ios/ArchiveUtilityBridge.m
  • Package.swift
  • sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/ArchiveUtility.kt
  • sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/build.gradle.kts
  • sdk/runanywhere-flutter/packages/runanywhere_onnx/pubspec.yaml
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Utilities/ArchiveUtility.swift
  • sdk/runanywhere-react-native/packages/core/src/services/index.ts
  • Package.resolved
  • sdk/runanywhere-react-native/packages/core/android/build.gradle
  • sdk/runanywhere-react-native/packages/core/ios/ArchiveUtility.swift
  • sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
  • sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Utilities/FileOperationsUtilities.swift

Comment on lines +125 to 136
const size_t num_samples = pcm_size / sizeof(float);

// Guard against WAV header overflow: data_size field is uint32_t (max ~4GB)
if (num_samples > UINT32_MAX / sizeof(int16_t)) {
return RAC_ERROR_INVALID_ARGUMENT;
}

// Int16 data size (2 bytes per sample)
const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * 2);
const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * sizeof(int16_t));

// Total WAV file size
const size_t wav_size = WAV_HEADER_SIZE + int16_data_size;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the float32 upper bound; it still permits RIFF-size overflow.

This check only guarantees the data chunk fits in uint32_t. build_wav_header() writes data_size + 36, so large inputs can still overflow the RIFF chunk size while passing validation here. wav_size = WAV_HEADER_SIZE + int16_data_size also remains free to wrap on 32-bit size_t.

🛠️ Suggested fix
+#include <limits>
+
-    // Guard against WAV header overflow: data_size field is uint32_t (max ~4GB)
-    if (num_samples > UINT32_MAX / sizeof(int16_t)) {
+    // Leave room for the RIFF chunk-size field: data_size + 36 must fit in uint32_t.
+    constexpr size_t kMaxRiffDataSize =
+        static_cast<size_t>(UINT32_MAX) - (WAV_HEADER_SIZE - 8);
+    if (num_samples > kMaxRiffDataSize / sizeof(int16_t)) {
         return RAC_ERROR_INVALID_ARGUMENT;
     }
 
     // Int16 data size (2 bytes per sample)
     const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * sizeof(int16_t));
 
     // Total WAV file size
+    if (int16_data_size > std::numeric_limits<size_t>::max() - WAV_HEADER_SIZE) {
+        return RAC_ERROR_INVALID_ARGUMENT;
+    }
     const size_t wav_size = WAV_HEADER_SIZE + int16_data_size;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/rac_audio_utils.cpp` around lines 125 - 136,
The current check only ensures int16_data_size fits in uint32_t but misses that
build_wav_header() adds 36 to form the RIFF chunk size and that wav_size can
wrap on 32-bit size_t; change the validation to compute int16_data_size as
before and then assert (uint64_t)int16_data_size + (uint64_t)36 <= UINT32_MAX to
prevent RIFF-size overflow, and compute wav_size using a wider type (e.g.,
uint64_t or size_t after checking) while also verifying the final wav_size does
not exceed SIZE_MAX before casting back; update the guard around
num_samples/int16_data_size, and reference WAV_HEADER_SIZE and
build_wav_header() in the check to ensure both the data chunk and RIFF chunk
sizes are validated.

Comment on lines +327 to +365
// Acquire lock only for state reads, release before long-running transcription
std::string transcription_id = generate_unique_id();
const char* model_id = rac_lifecycle_get_model_id(component->lifecycle);
const char* model_name = rac_lifecycle_get_model_name(component->lifecycle);
rac_handle_t service = nullptr;
rac_stt_options_t local_options;
rac_inference_framework_t framework;
int32_t sample_rate = 0;
const char* model_id = nullptr;
const char* model_name = nullptr;

// Debug: Log if model_id is null
if (!model_id) {
log_warning(
"STT.Component",
"rac_lifecycle_get_model_id returned null - model_id may not be set in telemetry");
} else {
log_debug("STT.Component", "STT transcription using model_id: %s", model_id);
{
std::lock_guard<std::mutex> lock(component->mtx);

model_id = rac_lifecycle_get_model_id(component->lifecycle);
model_name = rac_lifecycle_get_model_name(component->lifecycle);
framework = component->actual_framework;
sample_rate = component->config.sample_rate;

// Copy effective options to local so we can release the lock
local_options = options ? *options : component->default_options;

rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
if (result != RAC_SUCCESS) {
log_error("STT.Component", "No model loaded - cannot transcribe");

// Emit transcription failed event
rac_analytics_event_data_t event = {};
event.type = RAC_EVENT_STT_TRANSCRIPTION_FAILED;
event.data.stt_transcription = RAC_ANALYTICS_STT_TRANSCRIPTION_DEFAULT;
event.data.stt_transcription.transcription_id = transcription_id.c_str();
event.data.stt_transcription.model_id = model_id;
event.data.stt_transcription.model_name = model_name;
event.data.stt_transcription.error_code = result;
event.data.stt_transcription.error_message = "No model loaded";
rac_analytics_event_emit(RAC_EVENT_STT_TRANSCRIPTION_FAILED, &event);

return result;
}
}
// Lock released — safe to do long-running transcription
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find lifecycle header/source files to understand API contracts
fd -e h -e hpp -e c -e cpp 'lifecycle' sdk/runanywhere-commons | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 211


🏁 Script executed:

# Look for rac_lifecycle function definitions and documentation
rg -n 'rac_lifecycle_require_service|rac_lifecycle_get_model_id|rac_lifecycle_get_model_name' sdk/runanywhere-commons -A 3

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 15614


🏁 Script executed:

# Check for any retain/release/pin mechanisms in lifecycle layer
rg -n 'retain|release|refcount|pin|acquire' sdk/runanywhere-commons/src -i | grep -i lifecycle | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 542


🏁 Script executed:

# Get the full transcribe function to see all uses of borrowed state after unlock (lines 327-445)
sed -n '327,445p' sdk/runanywhere-commons/src/features/stt/stt_component.cpp | cat -n

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6701


🏁 Script executed:

# Get full rac_lifecycle_get_model_id and rac_lifecycle_get_model_name implementations
sed -n '314,340p' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | cat -n

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 957


🏁 Script executed:

# Get full rac_lifecycle_require_service implementation
sed -n '354,375p' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | cat -n

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 961


🏁 Script executed:

# Look at the LifecycleManager class definition to understand data storage and thread safety
rg -n 'class LifecycleManager|struct LifecycleManager' sdk/runanywhere-commons -A 30

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 3433


🏁 Script executed:

# Check for concurrent access to model_id and model_name during unload/destroy
rg -n 'model_id_|model_name_|current_service' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | head -30

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 825


Keep lifecycle-owned state alive for the entire transcription.

The model_id and model_name pointers captured at lines 339–340 are borrowed directly from LifecycleManager's internal std::string storage (via c_str()), and the lock is released immediately. These pointers are then used through line 407 in rac_stt_transcribe() and multiple telemetry events. Similarly, the service handle from rac_lifecycle_require_service() is a raw, unowned pointer with no reference counting. Concurrent calls to rac_lifecycle_unload() or rac_lifecycle_reset() can clear or modify those std::string fields, causing use-after-free. Copy model_id and model_name to owned std::string variables before unlocking, and either hold the mutex for the full operation or implement explicit pinning for the service handle per the component lifecycle contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` around lines 327
- 365, The code currently stores raw pointers model_id and model_name (from
rac_lifecycle_get_model_id/get_model_name) and a raw service handle (from
rac_lifecycle_require_service) then releases the lock, which can lead to
use-after-free; fix by copying the model strings into owned std::string locals
(e.g., local_model_id/local_model_name) while still under component->mtx and
keep using those owned strings after unlock, and ensure the service handle is
explicitly pinned/retained for the duration of rac_stt_transcribe (either by
using the lifecycle's pin/retain API or by holding the mutex for the full
transcription) and then release/unpin the service when done (adjust code around
rac_lifecycle_require_service, rac_stt_transcribe, and the telemetry emission
paths to use the owned strings and correctly release the service).

Comment on lines +330 to +357
const char* voice_id = nullptr;
const char* voice_name = nullptr;

// Debug: Log if voice_id is null
if (!voice_id) {
log_warning("TTS.Component",
"rac_lifecycle_get_model_id returned null - voice may not be set in telemetry");
} else {
log_debug("TTS.Component", "TTS synthesis using voice_id: %s", voice_id);
{
std::lock_guard<std::mutex> lock(component->mtx);

voice_id = rac_lifecycle_get_model_id(component->lifecycle);
voice_name = rac_lifecycle_get_model_name(component->lifecycle);
framework = component->actual_framework;

// Copy effective options to local so we can release the lock
local_options = options ? *options : component->default_options;

rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
if (result != RAC_SUCCESS) {
log_error("TTS.Component", "No voice loaded - cannot synthesize");
// Emit SYNTHESIS_FAILED event
rac_analytics_event_data_t event_data;
event_data.data.tts_synthesis = RAC_ANALYTICS_TTS_SYNTHESIS_DEFAULT;
event_data.data.tts_synthesis.synthesis_id = synthesis_id.c_str();
event_data.data.tts_synthesis.model_id = voice_id;
event_data.data.tts_synthesis.model_name = voice_name;
event_data.data.tts_synthesis.framework = framework;
event_data.data.tts_synthesis.error_code = result;
event_data.data.tts_synthesis.error_message = "No voice loaded";
rac_analytics_event_emit(RAC_EVENT_TTS_SYNTHESIS_FAILED, &event_data);
return result;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Dangling pointer risk: voice_id and voice_name used after lock release.

The pointers voice_id and voice_name are obtained from rac_lifecycle_get_model_id/name() inside the lock, but they're used after the lock is released (lines 365-367, 389-390, etc.). If another thread calls rac_tts_component_load_voice() or rac_tts_component_unload() concurrently, these pointers could become dangling.

Copy to std::string before releasing the lock:

🔒 Proposed fix to copy strings before releasing lock
     std::string synthesis_id = generate_uuid_v4();
     rac_handle_t service = nullptr;
     rac_tts_options_t local_options;
     rac_inference_framework_t framework;
-    const char* voice_id = nullptr;
-    const char* voice_name = nullptr;
+    std::string voice_id_str;
+    std::string voice_name_str;

     {
         std::lock_guard<std::mutex> lock(component->mtx);

-        voice_id = rac_lifecycle_get_model_id(component->lifecycle);
-        voice_name = rac_lifecycle_get_model_name(component->lifecycle);
+        const char* vid = rac_lifecycle_get_model_id(component->lifecycle);
+        const char* vname = rac_lifecycle_get_model_name(component->lifecycle);
+        voice_id_str = vid ? vid : "";
+        voice_name_str = vname ? vname : "";
         framework = component->actual_framework;

Then use voice_id_str.c_str() and voice_name_str.c_str() in the event emissions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 330
- 357, The code obtains voice_id and voice_name via rac_lifecycle_get_model_id /
rac_lifecycle_get_model_name while holding component->mtx but then uses those
raw const char* values after releasing the lock, risking dangling pointers; fix
by copying the strings into std::string locals (e.g., voice_id_str,
voice_name_str) while still inside the lock, and later use voice_id_str.c_str()
and voice_name_str.c_str() wherever voice_id and voice_name are currently used
(including in rac_analytics_event_data_t population and any logging), ensuring
no raw lifecycle pointers escape the locked region.

Comment on lines +448 to +477
const char* voice_id = nullptr;
const char* voice_name = nullptr;
int32_t char_count = static_cast<int32_t>(std::strlen(text));

{
std::lock_guard<std::mutex> lock(component->mtx);

voice_id = rac_lifecycle_get_model_id(component->lifecycle);
voice_name = rac_lifecycle_get_model_name(component->lifecycle);
framework = component->actual_framework;

// Copy effective options to local so we can release the lock
local_options = options ? *options : component->default_options;

rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service);
if (result != RAC_SUCCESS) {
log_error("TTS.Component", "No voice loaded - cannot synthesize stream");
// Emit SYNTHESIS_FAILED event
rac_analytics_event_data_t event_data;
event_data.data.tts_synthesis = RAC_ANALYTICS_TTS_SYNTHESIS_DEFAULT;
event_data.data.tts_synthesis.synthesis_id = synthesis_id.c_str();
event_data.data.tts_synthesis.model_id = voice_id;
event_data.data.tts_synthesis.model_name = voice_name;
event_data.data.tts_synthesis.framework = framework;
event_data.data.tts_synthesis.error_code = result;
event_data.data.tts_synthesis.error_message = "No voice loaded";
rac_analytics_event_emit(RAC_EVENT_TTS_SYNTHESIS_FAILED, &event_data);
return result;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same dangling pointer issue in synthesize_stream.

Apply the same fix here: copy voice_id and voice_name to std::string local variables before releasing the lock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 448
- 477, In synthesize_stream, avoid returning/using dangling const char* from
rac_lifecycle_get_model_id/name after releasing component->mtx: inside the
locked region (where voice_id/voice_name are assigned and local_options is
copied) immediately copy those values into std::string local variables (e.g.
voice_id_str, voice_name_str) and then release the lock; later use those
std::string.c_str() (or the std::string values) when filling event_data and
logging (references: synthesize_stream, voice_id, voice_name,
rac_lifecycle_get_model_id, rac_lifecycle_get_model_name, component->mtx,
local_options). Ensure all places that currently reference voice_id/voice_name
after the lock are updated to use the safe std::string locals.

Comment on lines +99 to 104
{
std::lock_guard<std::mutex> lock(handle->mutex);
for (auto& pair : handle->entries) { free_lora_entry(pair.second); }
handle->entries.clear();
}
delete handle;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Destroy is still unsafe under concurrent access.

This lock only serializes entries cleanup. A thread already waiting in another API on handle->mutex can continue after Line 104 and dereference a freed handle, so the change still leaves a use-after-free window. Please either make rac_lora_registry_destroy an externally synchronized operation and document that contract, or add explicit lifetime management before deleting the registry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`
around lines 99 - 104, The destroy path in rac_lora_registry_destroy is unsafe
because it deletes the handle while other APIs may still be blocked on or about
to use handle->mutex; fix by adding explicit lifetime management: add an
atomic<bool> destroying (or state) and an atomic<int> active_ops (or ref_count)
to the registry handle, update every public API that uses the handle (the
functions that lock handle->mutex and access handle->entries) to increment
active_ops at entry and decrement at exit (and check destroying to fail early),
and change rac_lora_registry_destroy to set destroying=true, wait (using a
condition_variable or busy-wait on active_ops==0) for active_ops to reach zero
while holding the mutex, then perform the entries cleanup (free_lora_entry on
each) and delete handle; alternatively, if you prefer the simpler contract,
document in rac_lora_registry_destroy's API that the caller must externally
synchronize all concurrent uses of the registry and do not change deletion
logic.

Comment on lines +229 to 251
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destinationPath.toNativeUtf8();

for (var i = 0; i < archive.files.length; i++) {
final file = archive.files[i];
final filename = file.name;
try {
final result = extractFn(
archivePathPtr,
destPathPtr,
nullptr, // default options
nullptr, // no progress callback
nullptr, // no user data
nullptr, // no result output
);

if (file.isFile) {
final outputFile = File('$destinationPath/$filename');
await outputFile.parent.create(recursive: true);
await outputFile.writeAsBytes(file.content as List<int>);
if (result != 0) {
throw SDKError.downloadFailed(
archivePath,
'Native extraction failed with code: $result',
);
}

onProgress?.call((i + 1) / totalFiles);
} finally {
calloc.free(archivePathPtr);
calloc.free(destPathPtr);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Dart FFI toNativeUtf8 default allocator

💡 Result:

String.toNativeUtf8() (from package:ffi/ffi.dart) uses malloc as its default allocator:

Pointer<Utf8> toNativeUtf8({Allocator allocator = malloc})

So if you don’t pass an allocator:, the returned pointer must be released with the same allocator, typically:

final p = 'hello'.toNativeUtf8();
malloc.free(p);

[1][2]


Memory allocator mismatch: allocated with malloc, freed with calloc.

toNativeUtf8() uses the malloc allocator by default, but the code frees the pointers using calloc.free(). This mismatch causes undefined behavior and can lead to memory corruption or crashes. Use malloc.free() to match the allocation method.

🐛 Proposed fix
   final archivePathPtr = archivePath.toNativeUtf8();
   final destPathPtr = destinationPath.toNativeUtf8();

   try {
     final result = extractFn(
       archivePathPtr,
       destPathPtr,
       nullptr, // default options
       nullptr, // no progress callback
       nullptr, // no user data
       nullptr, // no result output
     );

     if (result != 0) {
       throw SDKError.downloadFailed(
         archivePath,
         'Native extraction failed with code: $result',
       );
     }
   } finally {
-    calloc.free(archivePathPtr);
-    calloc.free(destPathPtr);
+    malloc.free(archivePathPtr);
+    malloc.free(destPathPtr);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destinationPath.toNativeUtf8();
for (var i = 0; i < archive.files.length; i++) {
final file = archive.files[i];
final filename = file.name;
try {
final result = extractFn(
archivePathPtr,
destPathPtr,
nullptr, // default options
nullptr, // no progress callback
nullptr, // no user data
nullptr, // no result output
);
if (file.isFile) {
final outputFile = File('$destinationPath/$filename');
await outputFile.parent.create(recursive: true);
await outputFile.writeAsBytes(file.content as List<int>);
if (result != 0) {
throw SDKError.downloadFailed(
archivePath,
'Native extraction failed with code: $result',
);
}
onProgress?.call((i + 1) / totalFiles);
} finally {
calloc.free(archivePathPtr);
calloc.free(destPathPtr);
}
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destinationPath.toNativeUtf8();
try {
final result = extractFn(
archivePathPtr,
destPathPtr,
nullptr, // default options
nullptr, // no progress callback
nullptr, // no user data
nullptr, // no result output
);
if (result != 0) {
throw SDKError.downloadFailed(
archivePath,
'Native extraction failed with code: $result',
);
}
} finally {
malloc.free(archivePathPtr);
malloc.free(destPathPtr);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx_download_strategy.dart`
around lines 229 - 251, The pointers returned by toNativeUtf8() (archivePathPtr
and destPathPtr in the extraction block) are allocated with malloc but are being
freed with calloc.free(), causing an allocator mismatch; update the finally
block to free those pointers with malloc.free() (use malloc.free(archivePathPtr)
and malloc.free(destPathPtr)) so allocation and deallocation use the same
allocator and avoid undefined behavior in the extractFn call and surrounding
code.

@VyasGuru
Copy link
Collaborator

VyasGuru commented Mar 6, 2026

Details IMG_0981 IMG_0978 IMG_0977 IMG_0976 IMG_0975 IMG_0974

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

♻️ Duplicate comments (3)
sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp (1)

74-80: ⚠️ Potential issue | 🟠 Major

Differentiate OOM from constructor failures.

Line 75’s new (std::nothrow) only returns nullptr on allocation failure. If rac_vad_analytics_s() throws during construction, the catch (...) path here still reports RAC_ERROR_OUT_OF_MEMORY, which gives callers the wrong failure reason and drops the exception details.

Suggested fix
-    try {
-        *out_handle = new (std::nothrow) rac_vad_analytics_s();
-    } catch (...) {
-        *out_handle = nullptr;
-    }
-    if (!*out_handle) {
-        return RAC_ERROR_OUT_OF_MEMORY;
-    }
+    try {
+        *out_handle = new (std::nothrow) rac_vad_analytics_s();
+    } catch (const std::exception& ex) {
+        *out_handle = nullptr;
+        log_error("VAD.Analytics", "Failed to create VAD analytics service: %s", ex.what());
+        return RAC_ERROR_INTERNAL;
+    } catch (...) {
+        *out_handle = nullptr;
+        log_error("VAD.Analytics", "Failed to create VAD analytics service: unknown exception");
+        return RAC_ERROR_INTERNAL;
+    }
+    if (!*out_handle) {
+        return RAC_ERROR_OUT_OF_MEMORY;
+    }
In C++, does `new (std::nothrow) T(...)` return `nullptr` when `T`'s constructor throws, or does it only return `nullptr` for allocation failure while constructor exceptions still propagate?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp` around lines 74 -
80, The current try/catch masks constructor exceptions as
RAC_ERROR_OUT_OF_MEMORY; change the logic around rac_vad_analytics_s creation so
allocation null-check and constructor exceptions are handled separately: use new
(std::nothrow) to attempt allocation, check if the returned pointer is nullptr
and return RAC_ERROR_OUT_OF_MEMORY, and separately catch constructor exceptions
(catch std::exception& to capture e.what() or a catch(...) fallback) to return a
distinct construction failure code (or propagate/log the exception) and set
*out_handle = nullptr; refer to symbols rac_vad_analytics_s, out_handle, and
RAC_ERROR_OUT_OF_MEMORY to locate and update the block.
sdk/runanywhere-commons/src/server/http_server.cpp (1)

87-113: ⚠️ Potential issue | 🟠 Major

Clean up partially initialized state if start() throws.

The new rac_server_start() catch block changes the return code, but it does not undo the mutations already made here. If loadModel() succeeds and a later operation on Line 94 or Line 112 throws, the singleton keeps a live llmHandle_ / server_ and the next start runs against half-initialized state.

Proposed fix
 rac_result_t HttpServer::start(const rac_server_config_t& config) {
     static constexpr int SERVER_START_POLL_ITERATIONS = 100;
     static constexpr int SERVER_START_POLL_MS = 100;

     {
         std::lock_guard<std::mutex> lock(mutex_);
+        try {
             if (running_) {
                 return RAC_ERROR_SERVER_ALREADY_RUNNING;
             }

             // ... validation and state copy ...

             rac_result_t rc = loadModel(modelPath_);
             if (RAC_FAILED(rc)) {
                 return rc;
             }

             server_ = std::make_unique<httplib::Server>();

             if (config.enable_cors == RAC_TRUE) {
                 setupCors();
             }

             setupRoutes();

             shouldStop_ = false;
             activeRequests_ = 0;
             totalRequests_ = 0;
             totalTokensGenerated_ = 0;
             startTime_ = std::chrono::steady_clock::now();

             serverThread_ = std::thread(&HttpServer::serverThread, this);
+        } catch (...) {
+            shouldStop_ = true;
+            if (serverThread_.joinable()) {
+                serverThread_.join();
+            }
+            unloadModel();
+            server_.reset();
+            throw;
+        }
     }
#!/bin/bash
# Verify that HttpServer::start performs throwing work after loadModel()
# and that cleanup is not handled inside the method.
sed -n '81,140p' sdk/runanywhere-commons/src/server/http_server.cpp
printf '\n--- throwing operations in start() ---\n'
rg -n "loadModel|std::make_unique|std::thread|setupRoutes|setupCors|catch" \
  sdk/runanywhere-commons/src/server/http_server.cpp -C 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 87 - 113,
The start() method can leave partially initialized members (llmHandle_, server_,
serverThread_, and state flags) set if a later operation throws after
loadModel() succeeds; update HttpServer::start to wrap the initialization steps
(loadModel, server_ = std::make_unique, setupCors, setupRoutes, starting
serverThread_) in a try/catch (or use RAII) and on any exception explicitly
undo/cleanup: unload or reset llmHandle_ (inverse of loadModel), reset server_
to nullptr, stop/join and reset serverThread_ if it was started, and restore
shouldStop_, activeRequests_, totalRequests_, totalTokensGenerated_ and
startTime_ to safe defaults before rethrowing or returning the error code so the
singleton cannot be left half-initialized.
sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp (1)

37-65: ⚠️ Potential issue | 🔴 Critical

Use one normalized safety check for entry paths and link targets.

is_path_safe() still allows a single leading backslash (\foo), and the symlink branch on Lines 236-245 bypasses the helper entirely. A crafted archive can still plant rooted Windows link targets even though regular entry paths are screened.

Also applies to: 236-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`
around lines 37 - 65, is_path_safe currently misses single leading backslashes
and the symlink extraction branch bypasses it, allowing rooted Windows link
targets; update is_path_safe to reject any path that begins with either '/' or
'\\' (not just UNC), keep the existing drive-letter and UNC checks, and ensure
the ".." component check treats both separators consistently; then call this
same normalized is_path_safe for both archive entry names and symlink targets
(replace the ad-hoc symlink handling in the symlink extraction branch with a
call to is_path_safe) so all targets use a single, unified validation routine.
🧹 Nitpick comments (6)
sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)

195-198: Consider consistent memory ordering for atomic operations.

The code mixes explicit memory_order_acquire loads (lines 185, 482) with implicit atomic operations that use memory_order_seq_cst (lines 195, 231, 251, 317, 384, 498). While seq_cst is correct, using explicit ordering throughout would be clearer:

  • Stores at lines 231 and 251 should use .store(value, std::memory_order_release) to pair with the acquire loads.
  • Reads at lines 195, 317, 384, 498 could use .load(std::memory_order_acquire) for consistency.
Proposed fix for consistent memory ordering
     // In rac_vad_component_initialize (line 195):
-    if (component->is_initialized) {
+    if (component->is_initialized.load(std::memory_order_acquire)) {

     // In rac_vad_component_initialize (line 231):
-    component->is_initialized = true;
+    component->is_initialized.store(true, std::memory_order_release);

     // In rac_vad_component_cleanup (line 251):
-    component->is_initialized = false;
+    component->is_initialized.store(false, std::memory_order_release);

Also applies to: 231-231, 251-251

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 195
- 198, The code mixes explicit acquire loads with default seq_cst atomics;
update the atomic operations for consistency: change the read of
component->is_initialized to use .load(std::memory_order_acquire) and change the
corresponding state stores (the .store(...) calls that set the
initialized/running flags in the VAD component) to use .store(value,
std::memory_order_release) so they pair with the acquire loads; apply the same
change to other atomic reads/ writes referenced in this file (the atomic state
flags used in the VAD init/start/stop logic) to use explicit
memory_order_acquire for loads and memory_order_release for stores.
sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp (1)

242-247: Consider marking all subscriptions as dead before clearing.

If reset_event_publisher() is ever called while a publish is in flight (even in tests), the alive flags in snapshotted subscriptions won't be updated. Consider iterating and setting alive->store(false) before clearing the containers for consistency with the unsubscribe pattern.

♻️ Suggested change
 void reset_event_publisher() {
     std::lock_guard<std::mutex> lock(g_event_mutex);
+    // Mark all subscriptions as dead before clearing
+    for (auto& [_, subs] : g_subscriptions) {
+        for (auto& s : subs) {
+            s.alive->store(false);
+        }
+    }
+    for (auto& s : g_all_subscriptions) {
+        s.alive->store(false);
+    }
     g_subscriptions.clear();
     g_all_subscriptions.clear();
     g_next_subscription_id.store(1);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp` around
lines 242 - 247, In reset_event_publisher(), before clearing g_subscriptions and
g_all_subscriptions, iterate the stored subscription entries (e.g., the entries
in g_all_subscriptions and/or g_subscriptions) and set each subscription's alive
flag to false (call alive->store(false)) while holding g_event_mutex so any
in-flight publish snapshots see the subscriptions as dead; then clear the
containers and reset g_next_subscription_id as before to mirror the unsubscribe
pattern.
sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (1)

377-382: Replace magic number with a named constant.

The literal 99 for RAC_ARCHIVE_STRUCTURE_UNKNOWN works correctly but reduces maintainability. If the C++ enum value changes, this code will silently break.

Consider adding this constant to model_types_cpp_bridge.dart alongside RacModelFormat and RacInferenceFramework:

Suggested constant definition (in model_types_cpp_bridge.dart)
/// Archive structure constants (rac_archive_structure_t)
abstract class RacArchiveStructure {
  static const int singleFileNested = 0;
  static const int directoryBased = 1;
  static const int nestedDirectory = 2;
  static const int unknown = 99;
}

Then update this call:

     final modelPath = DartBridgeDownload.findModelPathAfterExtraction(
       extractedDir: destDir,
-      structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
+      structure: RacArchiveStructure.unknown,
       framework: framework.toC(),
       format: format.toC(),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 377 - 382, Replace the magic number 99 passed to
DartBridgeDownload.findModelPathAfterExtraction with a named constant; add an
abstract RacArchiveStructure class in model_types_cpp_bridge.dart containing the
constants (singleFileNested, directoryBased, nestedDirectory, unknown) and then
call findModelPathAfterExtraction with RacArchiveStructure.unknown instead of 99
so the intent is clear and the value is centralized (refer to
DartBridgeDownload.findModelPathAfterExtraction and model_types_cpp_bridge.dart
/ RacArchiveStructure).
sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp (1)

458-474: Shared cancel_requested flag can affect concurrent requests.

The cancel_requested flag is shared across all generate calls. If concurrent generations are allowed (the lock is released before generation), calling cancel() will abort all in-flight generations, not just one.

If this is intentional (component-level cancellation), document it. If per-request cancellation is desired, consider adding a request-local cancel token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp`
around lines 458 - 474, The progress wrapper currently checks the shared
component-level flag ctx->component->cancel_requested in
diffusion_progress_wrapper, which will abort all concurrent generations; either
document that cancellation is component-wide or make it per-request by adding a
request-local cancel token inside diffusion_callback_context (e.g.,
request_cancelled or an atomic/rac_bool_t) and have diffusion::cancel() set that
token for the specific request instead of flipping component->cancel_requested;
update diffusion_callback_context, cancel(), and diffusion_progress_wrapper to
reference the new per-request token (or add clear comments explaining
component-level behavior) so cancellations only affect the intended generation.
sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp (1)

393-407: require_service doesn't pin the service—consider documenting or deprecating.

Unlike the new acquire_service, require_service returns the service handle without incrementing the refcount. This means the service can be unloaded while the caller is using it.

Consider:

  1. Documenting that require_service is only safe when the caller holds an external lock that prevents unload, OR
  2. Deprecating require_service in favor of acquire_service
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp` around
lines 393 - 407, The function rac_lifecycle_require_service returns a raw
service handle without incrementing its refcount (see
rac_lifecycle_require_service, LifecycleManager::current_service and state),
which lets the service be unloaded while in use; either document this safety
requirement or deprecate the API: mark rac_lifecycle_require_service as
deprecated and update callers to use the new acquire_service that pins the
service (or change rac_lifecycle_require_service to forward to acquire_service
and return a pinned handle), and add a clear comment on the function explaining
that callers must hold an external lock if they continue to use the unpinned
handle.
sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h (1)

237-254: Documentation should clarify edge cases and thread-safety.

The new acquire/release API is well-documented for the happy path, but consider adding:

  1. Underflow behavior: What happens if release_service is called without a prior acquire_service (refcount underflow)?
  2. Thread-safety: Explicitly state that these functions are thread-safe.
  3. Pairing requirement: Emphasize that each acquire_service call MUST be paired with exactly one release_service call.

As per coding guidelines, public C API headers must document error codes and lifecycle requirements.

📝 Suggested documentation additions
 /**
  * `@brief` Acquire (pin) the current service, preventing unload while held.
  *
  * Increments an internal refcount. The caller MUST call rac_lifecycle_release_service()
  * when done. Unload/destroy will block until all acquired references are released.
+ *
+ * Thread-safe: May be called concurrently from multiple threads.
+ *
+ * `@note` Each successful acquire MUST be paired with exactly one release.
+ *       Failure to release will cause unload/destroy to block indefinitely.
  *
  * `@param` handle Lifecycle manager handle
  * `@param` out_service Output: Service handle (pinned)
  * `@return` RAC_SUCCESS or RAC_ERROR_NOT_INITIALIZED if not loaded
  */

 /**
  * `@brief` Release a previously acquired service reference.
+ *
+ * Thread-safe: May be called concurrently from multiple threads.
+ *
+ * `@warning` Calling release without a prior acquire results in undefined behavior
+ *          (refcount underflow).
  *
  * `@param` handle Lifecycle manager handle
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h` around
lines 237 - 254, Update the documentation for rac_lifecycle_acquire_service and
rac_lifecycle_release_service to explicitly state thread-safety, the required
1:1 pairing (each rac_lifecycle_acquire_service must be matched by exactly one
rac_lifecycle_release_service), the behavior on refcount underflow (define
whether release without acquire is a no-op, asserts, or returns/logs an error),
and enumerate possible return/error codes (e.g., RAC_SUCCESS,
RAC_ERROR_NOT_INITIALIZED, and any error for invalid release), so callers know
lifecycle requirements and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sdk/runanywhere-commons/CMakeLists.txt`:
- Around line 177-180: The build currently disables LZMA (set ENABLE_LZMA OFF)
while the public API and detector still advertise and detect
RAC_ARCHIVE_TYPE_TAR_XZ, causing rac_detect_archive_type() to report .tar.xz but
rac_extract_archive_native() to fail at runtime; either enable LZMA by turning
ENABLE_LZMA ON in CMakeLists so libarchive has the xz decompressor, or
remove/guard RAC_ARCHIVE_TYPE_TAR_XZ from the public header and the detection
logic in rac_extraction.cpp (and any references in rac_detect_archive_type() and
rac_extract_archive_native()) so callers can no longer detect or select tar.xz
when the decompressor is unavailable.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h`:
- Around line 90-143: The header is missing lifecycle documentation for the
progress callback and input/output pointers; update the comments for
rac_extract_archive_native and rac_extraction_progress_fn to state that the
progress callback (rac_extraction_progress_fn) is invoked synchronously on the
calling thread during extraction, that user_data and the options pointer
(rac_extraction_options_t*) must remain valid and unchanged for the entire
duration of the rac_extract_archive_native call (and until the function returns)
because no background threads are used, and that out_result, if non-NULL, will
be populated only on successful completion (RAC_SUCCESS) and may be left
unmodified or only partially filled on failure—document the exact failure
semantics (e.g., zero/unchanged) so callers know how to handle it; also add a
brief note on rac_detect_archive_type that file_path and out_type must remain
valid only for the call and are not retained.

In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp`:
- Around line 1208-1211: configure_vad currently only updates config_ but does
not apply those settings to the live detector (sherpa_vad_), so process()
continues using old thresholds; modify configure_vad to acquire mutex_, set
config_, and then apply the new VAD parameters to the running detector — either
by calling the appropriate setter(s) on sherpa_vad_ or by
reinitializing/reloading it similarly to load_model() so threshold/window are
applied; ensure get_vad_config() remains consistent with the active detector and
protect changes with mutex_ (refer to configure_vad, load_model, process,
get_vad_config, and the sherpa_vad_ member).
- Around line 966-975: The code currently destroys/replaces sherpa_tts_ while
syntheses may be in progress, causing a dangling tts_ptr; modify the
reload/unload logic (where sherpa_tts_, new_tts, SherpaOnnxDestroyOfflineTts and
unload_model() are used) to either wait until active_synthesis_count_ == 0
before calling SherpaOnnxDestroyOfflineTts and assigning sherpa_tts_ = new_tts,
or convert sherpa_tts_ to a shared/ref-counted handle (e.g., shared_ptr-like
wrapper) so in-progress synthesis holds a reference and destruction waits for
last user; ensure the mutex still protects pointer swap and that
active_synthesis_count_ is incremented/decremented around synthesis start/finish
to coordinate the safe destroy/swap.

In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp`:
- Around line 267-270: The current blocking wait on mgr->service_cv using
mgr->service_refcount can hang forever if a release is missed; update the
unload()/reset() logic that uses mgr->service_cv.wait(lock, [mgr]{...}) to use a
timed wait (e.g., wait_for with a reasonable timeout) and after each timeout
check log a warning containing the current service_refcount and continue waiting
or break after a maximum number of retries; reference the same synchronization
objects (mgr->service_cv, mgr->mutex, mgr->service_refcount) and ensure the
warning log is emitted when a timeout occurs to aid debugging hung shutdowns.
- Around line 249-259: The refcount underflow in rac_lifecycle_release_service
must be prevented: modify rac_lifecycle_release_service to atomically decrement
service_refcount only if its current value is greater than zero (use an atomic
compare-exchange loop on LifecycleManager::service_refcount or load-and-compare
with compare_exchange_weak) and return immediately if it is already zero; when a
successful decrement yields previous==1 then call mgr->service_cv.notify_all(),
and optionally log or assert on an attempted release when count was zero to
surface misuse.

In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp`:
- Around line 781-804: The callback typedef rac_voice_agent_event_callback_fn
lacks required lifetime documentation: update the rac_voice_agent.h declaration
for rac_voice_agent_event_callback_fn (and the rac_voice_agent_event_t field
comments if present) to state explicitly that all pointers inside the event
(e.g., transcription, response, synthesized_audio) are owned by the caller and
valid only for the duration of the callback invocation and must not be retained
or freed by the callee; mention that the implementation will free those buffers
immediately after the callback returns so callers must copy any data they need
to keep.

In
`@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp`:
- Around line 459-476: The temp archive path generation in the needs_extraction
branch (where download_dest is constructed using downloads_dir + "/" + stem +
"." + ext, relying on get_file_extension and get_filename_stem and model_id) is
not task-unique and can collide across concurrent tasks; change this to produce
a unique per-task temp filename (e.g., incorporate a generated UUID, the task
ID, or use a secure temp-name API like mkstemp/mkstemp-like behavior) when
creating download_dest so concurrent downloads don't overwrite each other's temp
archives; apply the same change to the other occurrence around the 771-783 logic
that constructs a temp archive path.
- Around line 595-615: The loop constructing file_url and dest_path must
validate and sanitize each rac_model_file_descriptor_t before using
file.relative_path or file.destination_path: first check for nullptr and
non-empty strings before converting to std::string (avoid UB) and reject or skip
descriptors where both are missing; normalize backslashes to '/', reject
absolute paths (leading '/' or Windows roots like 'C:' or UNC '\\') and any
segment equal to ".." to prevent directory traversal, and only then join with
model_folder/base_download_url; also ensure mkdir_p() receives the sanitized
parent directory. Use the existing loop and local variables (file,
file.relative_path, file.destination_path, file_url, dest_path, model_folder,
base_download_url, mkdir_p) to locate where to add these checks and to log/skip
invalid descriptors.

In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`:
- Around line 143-145: The code copies rac_extraction_options_t opts but ignores
opts.archive_type_hint when initializing the reader (the reader is always set to
auto-detect); update the reader initialization logic in rac_extraction.cpp so
that if opts.archive_type_hint is set (not the sentinel/unknown value) you
configure the reader to use that explicit archive type instead of full
auto-detection—use the existing reader-config API you call on lines 164-166 but
branch on opts.archive_type_hint and pass the forced type to the reader
initialization (preserving auto-detect when archive_type_hint is unset).
- Around line 347-385: The function rac_detect_archive_type leaves the caller's
out_type unchanged on failure paths; update rac_detect_archive_type to set
*out_type to a safe default (e.g., RAC_ARCHIVE_TYPE_UNKNOWN or a defined
RAC_ARCHIVE_TYPE_NONE) before any early return (including nullptr checks, fopen
failure, fread short read, and final no-match return) so callers never see stale
enum values from previous calls; reference the out parameter out_type and the
function rac_detect_archive_type when making this change.
- Around line 308-327: The code currently ignores return values from
archive_write_finish_entry(ext) and archive_write_free(ext); update
rac_extraction.cpp to check both returns and set status =
RAC_ERROR_EXTRACTION_FAILED (and perform the same cleanup/logging used for other
I/O errors) when they return != ARCHIVE_OK instead of proceeding to report
success. Specifically, after calling archive_write_finish_entry(ext) in the
per-entry loop, test its return and handle failure exactly like the existing
data I/O error path (set status, break/cleanup); and before final return, call
archive_write_free(ext), check its return, and if not ARCHIVE_OK set status =
RAC_ERROR_EXTRACTION_FAILED so the function doesn’t return RAC_SUCCESS on silent
write/close failures.

In `@sdk/runanywhere-commons/src/server/http_server.cpp`:
- Around line 226-230: The code currently invokes callbacks (requestCallback_,
errorCallback_) while holding callback_mutex_, which can deadlock if those
callbacks call rac_server_set_request_callback() or
rac_server_set_error_callback() and also serializes request handling; fix by
under callback_mutex_ copying the function pointers and user-data (e.g., copy
requestCallback_, requestCallbackUserData_, errorCallback_,
errorCallbackUserData_ into local variables) and then release the lock and
invoke the copied callbacks (for the GET "/v1/models" path, the POST path, and
the error path) outside the lock so the mutex is not held during external
callback execution.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 364-367: The throw uses an undefined DownloadException causing a
compile error; change the thrown type to the standard Exception and keep the
same message. Update the native extraction failure block (where _logger.error
logs 'Native extraction failed with code: $result' and the subsequent throw) to
throw Exception('Native extraction failed with code: $result') instead of
DownloadException; this aligns with other throws in this file that use
Exception.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 265-268: The checkStorage method currently treats a null response
from checkStorageAvailability as “available” which lets downloads proceed when
the bridge or native symbol is missing; change checkStorage to fail closed by
returning false when checkStorageAvailability returns null or throws: call
checkStorageAvailability(requiredBytes), if the result is null return false,
otherwise return result.isAvailable; optionally catch and log any exceptions
from checkStorageAvailability and return false to ensure downloads are blocked
on lookup/registration failures (refer to checkStorage and
checkStorageAvailability).
- Around line 398-410: The loop allocates a contiguous buffer (entries via
calloc<Pointer<Utf8>>(count)) and individual Utf8 strings (name.toNativeUtf8())
but on exception the catch returns without freeing any of those, leaking native
memory; update the error path so that if an exception occurs during the for-loop
you iterate over the already-allocated indices (0..i-1) and free each entries[j]
(the pointers returned by toNativeUtf8()) and then free the entries buffer
itself (calloc.free(entries) or the appropriate allocator free), before setting
outEntries.value = nullptr, outCount.value = 0 and returning _errorFileNotFound;
apply this cleanup inside the existing try/catch around the loop that references
contents, entries, toNativeUtf8, outEntries, outCount and _errorFileNotFound.
- Around line 102-104: The native call in dart_bridge_file_manager.dart
currently ignores the native return code (calling fn(_callbacksPtr!, pathPtr,
sizePtr)) and always returns sizePtr.value, which collapses failures into a
zero-size result; update the call sites (including the method shown and the
similar implementations in modelsStorageUsed() and cacheSize()) to check the
native function's return value, handle non-success results by throwing or
returning an error/nullable result instead of returning sizePtr.value, and only
read and return sizePtr.value when the native return indicates success (use the
exact symbols _callbacksPtr, fn, pathPtr, sizePtr, modelsStorageUsed, cacheSize
to find the spots).

In
`@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`:
- Around line 129-140: The code in the readdir loop (using entry, strdup and
entries) doesn't check the result of strdup, so a failed allocation yields a
null entry and the function still returns RAC_SUCCESS; update the loop in
FileManagerBridge.cpp to check the return of strdup(entry->d_name) each
iteration, and on failure free any previously strdup'd strings and the entries
array, closedir(dir), set *outEntries and *outCount appropriately (e.g.,
null/0), and return a failure code (not RAC_SUCCESS) so callers don't receive a
partially populated entries array; ensure you reference entries, entry, strdup,
closedir, outEntries, outCount and replace the final RAC_SUCCESS return with an
error path when strdup fails.
- Around line 53-56: The current mkdir handling in FileManagerBridge.cpp returns
RAC_SUCCESS whenever mkdir() succeeds or errno == EEXIST, which incorrectly
treats existing files/symlinks as a created directory; update the logic so that
when mkdir fails with errno == EEXIST you call stat/lstat on path and verify
S_ISDIR(st_mode) before returning RAC_SUCCESS, otherwise return
RAC_ERROR_FILE_IO; keep the existing return values (RAC_SUCCESS /
RAC_ERROR_FILE_IO) and use the same variables (path, errno) and functions
(mkdir) so the change is limited to adding the directory check.
- Around line 172-194: The space callbacks posixGetAvailableSpace and
posixGetTotalSpace currently return 0 on statvfs() failure which violates the
callback contract (should return -1 on error) and can be mistaken for valid
space; update both functions so that when statvfs(mountPoint, &vfs) != 0 they
return a signed error sentinel (e.g., static_cast<int64_t>(-1)) instead of 0,
preserving the existing logic and types for the successful return path.

---

Duplicate comments:
In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp`:
- Around line 74-80: The current try/catch masks constructor exceptions as
RAC_ERROR_OUT_OF_MEMORY; change the logic around rac_vad_analytics_s creation so
allocation null-check and constructor exceptions are handled separately: use new
(std::nothrow) to attempt allocation, check if the returned pointer is nullptr
and return RAC_ERROR_OUT_OF_MEMORY, and separately catch constructor exceptions
(catch std::exception& to capture e.what() or a catch(...) fallback) to return a
distinct construction failure code (or propagate/log the exception) and set
*out_handle = nullptr; refer to symbols rac_vad_analytics_s, out_handle, and
RAC_ERROR_OUT_OF_MEMORY to locate and update the block.

In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`:
- Around line 37-65: is_path_safe currently misses single leading backslashes
and the symlink extraction branch bypasses it, allowing rooted Windows link
targets; update is_path_safe to reject any path that begins with either '/' or
'\\' (not just UNC), keep the existing drive-letter and UNC checks, and ensure
the ".." component check treats both separators consistently; then call this
same normalized is_path_safe for both archive entry names and symlink targets
(replace the ad-hoc symlink handling in the symlink extraction branch with a
call to is_path_safe) so all targets use a single, unified validation routine.

In `@sdk/runanywhere-commons/src/server/http_server.cpp`:
- Around line 87-113: The start() method can leave partially initialized members
(llmHandle_, server_, serverThread_, and state flags) set if a later operation
throws after loadModel() succeeds; update HttpServer::start to wrap the
initialization steps (loadModel, server_ = std::make_unique, setupCors,
setupRoutes, starting serverThread_) in a try/catch (or use RAII) and on any
exception explicitly undo/cleanup: unload or reset llmHandle_ (inverse of
loadModel), reset server_ to nullptr, stop/join and reset serverThread_ if it
was started, and restore shouldStop_, activeRequests_, totalRequests_,
totalTokensGenerated_ and startTime_ to safe defaults before rethrowing or
returning the error code so the singleton cannot be left half-initialized.

---

Nitpick comments:
In `@sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h`:
- Around line 237-254: Update the documentation for
rac_lifecycle_acquire_service and rac_lifecycle_release_service to explicitly
state thread-safety, the required 1:1 pairing (each
rac_lifecycle_acquire_service must be matched by exactly one
rac_lifecycle_release_service), the behavior on refcount underflow (define
whether release without acquire is a no-op, asserts, or returns/logs an error),
and enumerate possible return/error codes (e.g., RAC_SUCCESS,
RAC_ERROR_NOT_INITIALIZED, and any error for invalid release), so callers know
lifecycle requirements and error handling.

In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp`:
- Around line 393-407: The function rac_lifecycle_require_service returns a raw
service handle without incrementing its refcount (see
rac_lifecycle_require_service, LifecycleManager::current_service and state),
which lets the service be unloaded while in use; either document this safety
requirement or deprecate the API: mark rac_lifecycle_require_service as
deprecated and update callers to use the new acquire_service that pins the
service (or change rac_lifecycle_require_service to forward to acquire_service
and return a pinned handle), and add a clear comment on the function explaining
that callers must hold an external lock if they continue to use the unpinned
handle.

In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp`:
- Around line 458-474: The progress wrapper currently checks the shared
component-level flag ctx->component->cancel_requested in
diffusion_progress_wrapper, which will abort all concurrent generations; either
document that cancellation is component-wide or make it per-request by adding a
request-local cancel token inside diffusion_callback_context (e.g.,
request_cancelled or an atomic/rac_bool_t) and have diffusion::cancel() set that
token for the specific request instead of flipping component->cancel_requested;
update diffusion_callback_context, cancel(), and diffusion_progress_wrapper to
reference the new per-request token (or add clear comments explaining
component-level behavior) so cancellations only affect the intended generation.

In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp`:
- Around line 195-198: The code mixes explicit acquire loads with default
seq_cst atomics; update the atomic operations for consistency: change the read
of component->is_initialized to use .load(std::memory_order_acquire) and change
the corresponding state stores (the .store(...) calls that set the
initialized/running flags in the VAD component) to use .store(value,
std::memory_order_release) so they pair with the acquire loads; apply the same
change to other atomic reads/ writes referenced in this file (the atomic state
flags used in the VAD init/start/stop logic) to use explicit
memory_order_acquire for loads and memory_order_release for stores.

In `@sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp`:
- Around line 242-247: In reset_event_publisher(), before clearing
g_subscriptions and g_all_subscriptions, iterate the stored subscription entries
(e.g., the entries in g_all_subscriptions and/or g_subscriptions) and set each
subscription's alive flag to false (call alive->store(false)) while holding
g_event_mutex so any in-flight publish snapshots see the subscriptions as dead;
then clear the containers and reset g_next_subscription_id as before to mirror
the unsubscribe pattern.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 377-382: Replace the magic number 99 passed to
DartBridgeDownload.findModelPathAfterExtraction with a named constant; add an
abstract RacArchiveStructure class in model_types_cpp_bridge.dart containing the
constants (singleFileNested, directoryBased, nestedDirectory, unknown) and then
call findModelPathAfterExtraction with RacArchiveStructure.unknown instead of 99
so the intent is clear and the value is centralized (refer to
DartBridgeDownload.findModelPathAfterExtraction and model_types_cpp_bridge.dart
/ RacArchiveStructure).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe26c885-f15f-4e24-b3b9-b59440bf7591

📥 Commits

Reviewing files that changed from the base of the PR and between 67b4c22 and c2bc42f.

📒 Files selected for processing (18)
  • sdk/runanywhere-commons/CMakeLists.txt
  • sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h
  • sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h
  • sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp
  • sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp
  • sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp
  • sdk/runanywhere-commons/src/features/llm/llm_component.cpp
  • sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp
  • sdk/runanywhere-commons/src/features/vad/vad_component.cpp
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
  • sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp
  • sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp
  • sdk/runanywhere-commons/src/server/http_server.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp

Comment on lines +177 to +180
set(ENABLE_LZMA OFF CACHE BOOL "" FORCE) # tar.xz not currently used by any model
set(ENABLE_ZSTD OFF CACHE BOOL "" FORCE)
set(ENABLE_ZLIB ON CACHE BOOL "" FORCE) # Needed for tar.gz and zip
set(ENABLE_BZip2 ON CACHE BOOL "" FORCE) # Needed for tar.bz2 (k2-fsa models)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'ENABLE_LZMA|ENABLE_ZLIB|ENABLE_BZip2' sdk/runanywhere-commons/CMakeLists.txt
rg -n 'TAR_XZ|tar\.xz|Supports ZIP, TAR.GZ, TAR.BZ2, TAR.XZ' \
  sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h \
  sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp \
  sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_types.h

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1005


🏁 Script executed:

sed -n '1,15p' sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 577


🏁 Script executed:

sed -n '375,385p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 392


🏁 Script executed:

rg -n 'RAC_ARCHIVE_TYPE_TAR_XZ' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -A 5 -B 5

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 413


🏁 Script executed:

# Check if there's any conditional handling based on LZMA/XZLIB availability
rg -n 'LZMA|xz|XZLIB|lzma' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 110


🏁 Script executed:

# Find the main extraction function
rg -n 'rac_extract\|extract_archive' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

# Look for where archive_read is called and how it processes the detected type
rg -n 'archive_read|archive_write|archive_read_support' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -A 3 | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1632


🏁 Script executed:

# Check if the detected archive type is actually used in the extraction logic
rg -n 'out_type\|archive_type' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -B 3 -A 3

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

# Find the detect_archive_type function definition and caller
ast-grep --pattern 'rac_detect_archive_type($_) {
  $$$
}'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

# Find the main extraction function and its signature
rg -n 'rac_extract\|RAC_ERROR_EXTRACTION\|RAC_ERROR_UNSUPPORTED' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | head -30

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

# Look at the file structure - find function names
rg -n '^\w+.*\(' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | grep -E 'detect|extract'

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 266


🏁 Script executed:

sed -n '127,180p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2244


🏁 Script executed:

sed -n '347,400p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1286


🏁 Script executed:

# Check if rac_detect_archive_type is in public API
rg -n 'rac_detect_archive_type' sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 174


🏁 Script executed:

# Check all usages of rac_detect_archive_type in codebase
rg -rn 'rac_detect_archive_type' sdk/

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2252


🏁 Script executed:

rg -n 'TAR_XZ\|tar\.xz' sdk/runanywhere-commons/tests/test_extraction.cpp -B 2 -A 2

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

grep -n 'TAR_GZ\|TAR_BZ2\|ZIP\|archive' sdk/runanywhere-commons/tests/test_extraction.cpp | head -30

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2091


🏁 Script executed:

rg -n 'tar\.bz2\|BZ2' sdk/runanywhere-commons/tests/test_extraction.cpp | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 56


🏁 Script executed:

# Check the CMakeLists.txt in tests to see what's being used
cat sdk/runanywhere-commons/tests/CMakeLists.txt

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 7786


Fix the broken TAR.XZ support contract: disable the advertised format or enable LZMA decompression.

Lines 177-180 set ENABLE_LZMA to OFF, but the public extraction API and detector still expose RAC_ARCHIVE_TYPE_TAR_XZ in the header (line 6) and detection code (lines 378-382 in rac_extraction.cpp). This creates a broken contract: callers can detect .tar.xz files via rac_detect_archive_type(), but rac_extract_archive_native() will fail at runtime because libarchive lacks the xz decompression filter. The test suite does not cover tar.xz extraction, so the failure isn't caught.

Either enable LZMA by setting ENABLE_LZMA ON, or remove RAC_ARCHIVE_TYPE_TAR_XZ from the public API and detection logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/CMakeLists.txt` around lines 177 - 180, The build
currently disables LZMA (set ENABLE_LZMA OFF) while the public API and detector
still advertise and detect RAC_ARCHIVE_TYPE_TAR_XZ, causing
rac_detect_archive_type() to report .tar.xz but rac_extract_archive_native() to
fail at runtime; either enable LZMA by turning ENABLE_LZMA ON in CMakeLists so
libarchive has the xz decompressor, or remove/guard RAC_ARCHIVE_TYPE_TAR_XZ from
the public header and the detection logic in rac_extraction.cpp (and any
references in rac_detect_archive_type() and rac_extract_archive_native()) so
callers can no longer detect or select tar.xz when the decompressor is
unavailable.

Comment on lines +90 to +143
/**
* @brief Progress callback for extraction.
*
* @param files_extracted Number of files extracted so far
* @param total_files Total files in archive (0 if unknown for streaming formats)
* @param bytes_extracted Bytes written to disk so far
* @param user_data User-provided context
*/
typedef void (*rac_extraction_progress_fn)(int32_t files_extracted, int32_t total_files,
int64_t bytes_extracted, void* user_data);

// =============================================================================
// EXTRACTION API
// =============================================================================

/**
* @brief Extract an archive using native libarchive.
*
* Performs streaming extraction with constant memory usage.
* Auto-detects archive format from magic bytes if archive_type_hint
* is RAC_ARCHIVE_TYPE_NONE.
*
* @param archive_path Path to the archive file
* @param destination_dir Directory to extract into (created if needed)
* @param options Extraction options (NULL for defaults)
* @param progress_callback Progress callback (can be NULL)
* @param user_data Context for progress callback
* @param out_result Output: extraction statistics (can be NULL)
* @return RAC_SUCCESS on success, error code on failure
*
* Error codes:
* - RAC_ERROR_EXTRACTION_FAILED: General extraction error
* - RAC_ERROR_UNSUPPORTED_ARCHIVE: Unrecognized archive format
* - RAC_ERROR_FILE_NOT_FOUND: Archive file does not exist
* - RAC_ERROR_NULL_POINTER: archive_path or destination_dir is NULL
*/
RAC_API rac_result_t rac_extract_archive_native(const char* archive_path,
const char* destination_dir,
const rac_extraction_options_t* options,
rac_extraction_progress_fn progress_callback,
void* user_data,
rac_extraction_result_t* out_result);

/**
* @brief Detect archive type from file magic bytes.
*
* Reads the first few bytes of the file to determine the archive format.
* More reliable than file extension detection.
*
* @param file_path Path to the file
* @param out_type Output: detected archive type
* @return RAC_TRUE if archive type detected, RAC_FALSE otherwise
*/
RAC_API rac_bool_t rac_detect_archive_type(const char* file_path, rac_archive_type_t* out_type);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document callback and output lifetimes in this public header.

The error codes are listed, but the lifecycle requirements are still missing: callers need to know whether progress_callback is synchronous, how long user_data / options / output buffers must stay valid, and what state the outputs are left in on failure.

As per coding guidelines, "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h`
around lines 90 - 143, The header is missing lifecycle documentation for the
progress callback and input/output pointers; update the comments for
rac_extract_archive_native and rac_extraction_progress_fn to state that the
progress callback (rac_extraction_progress_fn) is invoked synchronously on the
calling thread during extraction, that user_data and the options pointer
(rac_extraction_options_t*) must remain valid and unchanged for the entire
duration of the rac_extract_archive_native call (and until the function returns)
because no background threads are used, and that out_result, if non-NULL, will
be populated only on successful completion (RAC_SUCCESS) and may be left
unmodified or only partially filled on failure—document the exact failure
semantics (e.g., zero/unchanged) so callers know how to handle it; also add a
brief note on rac_detect_archive_type that file_path and out_type must remain
valid only for the call and are not retained.

Comment on lines +966 to 975
// Workaround for sherpa-onnx std::once_flag issue: espeak_Initialize is
// only called internally on the very first SherpaOnnxCreateOfflineTts call.
// When switching TTS models with different data_dir, destroy and recreate
// the instance so the config (including data_dir) is applied correctly.
if (sherpa_tts_ && sherpa_tts_ != new_tts) {
SherpaOnnxDestroyOfflineTts(sherpa_tts_);
sherpa_tts_ = nullptr;
}
sherpa_tts_ = new_tts;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Prevent reload/unload from freeing sherpa_tts_ during synthesis.

Lines 1046-1074 copy the raw sherpa_tts_ pointer out of the mutex and use it after unlock. This reload path and unload_model() can destroy that same instance meanwhile, so concurrent model switches can turn tts_ptr into a dangling pointer mid-synthesis. Please wait for active_synthesis_count_ to reach zero before destroying/replacing the handle, or move the TTS instance behind shared ownership/ref-counting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp` around lines 966
- 975, The code currently destroys/replaces sherpa_tts_ while syntheses may be
in progress, causing a dangling tts_ptr; modify the reload/unload logic (where
sherpa_tts_, new_tts, SherpaOnnxDestroyOfflineTts and unload_model() are used)
to either wait until active_synthesis_count_ == 0 before calling
SherpaOnnxDestroyOfflineTts and assigning sherpa_tts_ = new_tts, or convert
sherpa_tts_ to a shared/ref-counted handle (e.g., shared_ptr-like wrapper) so
in-progress synthesis holds a reference and destruction waits for last user;
ensure the mutex still protects pointer swap and that active_synthesis_count_ is
incremented/decremented around synthesis start/finish to coordinate the safe
destroy/swap.

Comment on lines 1208 to 1211
bool ONNXVAD::configure_vad(const VADConfig& config) {
std::lock_guard<std::mutex> lock(mutex_);
config_ = config;
return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

configure_vad() still doesn't reconfigure the live detector.

This only updates config_, but process() keeps using the already-created sherpa_vad_, and load_model() is still the only place that applies threshold/window settings to Sherpa. That means callers can see get_vad_config() report values that were never applied to detection.

Also applies to: 1286-1288

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp` around lines 1208
- 1211, configure_vad currently only updates config_ but does not apply those
settings to the live detector (sherpa_vad_), so process() continues using old
thresholds; modify configure_vad to acquire mutex_, set config_, and then apply
the new VAD parameters to the running detector — either by calling the
appropriate setter(s) on sherpa_vad_ or by reinitializing/reloading it similarly
to load_model() so threshold/window are applied; ensure get_vad_config() remains
consistent with the active detector and protect changes with mutex_ (refer to
configure_vad, load_model, process, get_vad_config, and the sherpa_vad_ member).

Comment on lines +249 to +259
void rac_lifecycle_release_service(rac_handle_t handle) {
if (handle == nullptr) {
return;
}

auto* mgr = static_cast<LifecycleManager*>(handle);
int prev = mgr->service_refcount.fetch_sub(1);
if (prev <= 1) {
mgr->service_cv.notify_all();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Refcount underflow is unprotected.

If rac_lifecycle_release_service is called without a prior acquire_service, the refcount underflows:

int prev = mgr->service_refcount.fetch_sub(1);  // underflows to -1
if (prev <= 1) {
    mgr->service_cv.notify_all();  // notifies even on underflow
}

This corrupts state and could cause unload() to proceed prematurely on subsequent operations.

🛡️ Proposed fix: Add underflow check
 void rac_lifecycle_release_service(rac_handle_t handle) {
     if (handle == nullptr) {
         return;
     }

     auto* mgr = static_cast<LifecycleManager*>(handle);
     int prev = mgr->service_refcount.fetch_sub(1);
+    if (prev <= 0) {
+        // Underflow - caller released without acquiring
+        RAC_LOG_ERROR(mgr->logger_category.c_str(),
+                      "release_service called without matching acquire (refcount was %d)", prev);
+        mgr->service_refcount.fetch_add(1);  // Restore to prevent further corruption
+        return;
+    }
     if (prev <= 1) {
         mgr->service_cv.notify_all();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp` around
lines 249 - 259, The refcount underflow in rac_lifecycle_release_service must be
prevented: modify rac_lifecycle_release_service to atomically decrement
service_refcount only if its current value is greater than zero (use an atomic
compare-exchange loop on LifecycleManager::service_refcount or load-and-compare
with compare_exchange_weak) and return immediately if it is already zero; when a
successful decrement yields previous==1 then call mgr->service_cv.notify_all(),
and optionally log or assert on an attempted release when count was zero to
surface misuse.

Comment on lines +398 to +410
final entries = calloc<Pointer<Utf8>>(count);
for (var i = 0; i < count; i++) {
final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty);
entries[i] = name.toNativeUtf8();
}

outEntries.value = entries;
outCount.value = count;
return RacResultCode.success;
} catch (_) {
outEntries.value = nullptr;
outCount.value = 0;
return _errorFileNotFound;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clean up partially allocated entry buffers on error.

Once entries and some entries[i] strings are allocated, any exception in the loop drops into the outer catch and leaks native memory. Free the successfully allocated elements before returning the error.

Suggested fix
     final entries = calloc<Pointer<Utf8>>(count);
+    var allocated = 0;
     for (var i = 0; i < count; i++) {
       final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty);
       entries[i] = name.toNativeUtf8();
+      allocated++;
     }

     outEntries.value = entries;
     outCount.value = count;
     return RacResultCode.success;
   } catch (_) {
+    for (var i = 0; i < allocated; i++) {
+      calloc.free(entries[i]);
+    }
+    calloc.free(entries);
     outEntries.value = nullptr;
     outCount.value = 0;
     return _errorFileNotFound;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 398 - 410, The loop allocates a contiguous buffer (entries via
calloc<Pointer<Utf8>>(count)) and individual Utf8 strings (name.toNativeUtf8())
but on exception the catch returns without freeing any of those, leaking native
memory; update the error path so that if an exception occurs during the for-loop
you iterate over the already-allocated indices (0..i-1) and free each entries[j]
(the pointers returned by toNativeUtf8()) and then free the entries buffer
itself (calloc.free(entries) or the appropriate allocator free), before setting
outEntries.value = nullptr, outCount.value = 0 and returning _errorFileNotFound;
apply this cleanup inside the existing try/catch around the loop that references
contents, entries, toNativeUtf8, outEntries, outCount and _errorFileNotFound.

Comment on lines +53 to +56
if (mkdir(path, 0755) == 0 || errno == EEXIST) {
return RAC_SUCCESS;
}
return RAC_ERROR_FILE_IO;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only accept EEXIST when the target is actually a directory.

Line 53 currently treats any EEXIST as success, including the case where path is an existing file or symlink. That makes the callback lie about having created a directory and pushes the failure downstream.

Proposed fix
-    if (mkdir(path, 0755) == 0 || errno == EEXIST) {
-        return RAC_SUCCESS;
-    }
+    if (mkdir(path, 0755) == 0) {
+        return RAC_SUCCESS;
+    }
+    if (errno == EEXIST) {
+        struct stat st;
+        if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+            return RAC_SUCCESS;
+        }
+    }
     return RAC_ERROR_FILE_IO;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (mkdir(path, 0755) == 0 || errno == EEXIST) {
return RAC_SUCCESS;
}
return RAC_ERROR_FILE_IO;
if (mkdir(path, 0755) == 0) {
return RAC_SUCCESS;
}
if (errno == EEXIST) {
struct stat st;
if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
return RAC_SUCCESS;
}
}
return RAC_ERROR_FILE_IO;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 53 - 56, The current mkdir handling in FileManagerBridge.cpp
returns RAC_SUCCESS whenever mkdir() succeeds or errno == EEXIST, which
incorrectly treats existing files/symlinks as a created directory; update the
logic so that when mkdir fails with errno == EEXIST you call stat/lstat on path
and verify S_ISDIR(st_mode) before returning RAC_SUCCESS, otherwise return
RAC_ERROR_FILE_IO; keep the existing return values (RAC_SUCCESS /
RAC_ERROR_FILE_IO) and use the same variables (path, errno) and functions
(mkdir) so the change is limited to adding the directory check.

Comment on lines +129 to +140
while ((entry = readdir(dir)) != nullptr && i < count) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
entries[i] = strdup(entry->d_name);
i++;
}
closedir(dir);

*outEntries = entries;
*outCount = i;
return RAC_SUCCESS;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast on partial strdup() failure.

Line 133 doesn't check strdup(). If one entry allocation fails, this returns RAC_SUCCESS with a null name in entries, which can crash the file-manager walk later.

Proposed fix
     while ((entry = readdir(dir)) != nullptr && i < count) {
         if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
             continue;
         }
         entries[i] = strdup(entry->d_name);
+        if (!entries[i]) {
+            for (size_t j = 0; j < i; ++j) {
+                free(entries[j]);
+            }
+            free(entries);
+            closedir(dir);
+            return RAC_ERROR_FILE_IO;
+        }
         i++;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while ((entry = readdir(dir)) != nullptr && i < count) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
entries[i] = strdup(entry->d_name);
i++;
}
closedir(dir);
*outEntries = entries;
*outCount = i;
return RAC_SUCCESS;
while ((entry = readdir(dir)) != nullptr && i < count) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
entries[i] = strdup(entry->d_name);
if (!entries[i]) {
for (size_t j = 0; j < i; ++j) {
free(entries[j]);
}
free(entries);
closedir(dir);
return RAC_ERROR_FILE_IO;
}
i++;
}
closedir(dir);
*outEntries = entries;
*outCount = i;
return RAC_SUCCESS;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 129 - 140, The code in the readdir loop (using entry, strdup and
entries) doesn't check the result of strdup, so a failed allocation yields a
null entry and the function still returns RAC_SUCCESS; update the loop in
FileManagerBridge.cpp to check the return of strdup(entry->d_name) each
iteration, and on failure free any previously strdup'd strings and the entries
array, closedir(dir), set *outEntries and *outCount appropriately (e.g.,
null/0), and return a failure code (not RAC_SUCCESS) so callers don't receive a
partially populated entries array; ensure you reference entries, entry, strdup,
closedir, outEntries, outCount and replace the final RAC_SUCCESS return with an
error path when strdup fails.

Comment on lines +172 to +194
static int64_t posixGetAvailableSpace(void* /*userData*/) {
// Use root "/" on iOS, "/data" on Android
#if defined(ANDROID) || defined(__ANDROID__)
const char* mountPoint = "/data";
#else
const char* mountPoint = "/";
#endif

struct statvfs vfs;
if (statvfs(mountPoint, &vfs) != 0) return 0;
return static_cast<int64_t>(vfs.f_bavail) * static_cast<int64_t>(vfs.f_frsize);
}

static int64_t posixGetTotalSpace(void* /*userData*/) {
#if defined(ANDROID) || defined(__ANDROID__)
const char* mountPoint = "/data";
#else
const char* mountPoint = "/";
#endif

struct statvfs vfs;
if (statvfs(mountPoint, &vfs) != 0) return 0;
return static_cast<int64_t>(vfs.f_blocks) * static_cast<int64_t>(vfs.f_frsize);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return the documented error sentinel from the space callbacks.

Lines 181 and 193 return 0 when statvfs() fails, but the callback contract is -1 on error. Returning 0 hides failures as valid storage values.

Proposed fix
     struct statvfs vfs;
-    if (statvfs(mountPoint, &vfs) != 0) return 0;
+    if (statvfs(mountPoint, &vfs) != 0) return -1;
     return static_cast<int64_t>(vfs.f_bavail) * static_cast<int64_t>(vfs.f_frsize);
@@
     struct statvfs vfs;
-    if (statvfs(mountPoint, &vfs) != 0) return 0;
+    if (statvfs(mountPoint, &vfs) != 0) return -1;
     return static_cast<int64_t>(vfs.f_blocks) * static_cast<int64_t>(vfs.f_frsize);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 172 - 194, The space callbacks posixGetAvailableSpace and
posixGetTotalSpace currently return 0 on statvfs() failure which violates the
callback contract (should return -1 on error) and can be mistaken for valid
space; update both functions so that when statvfs(mountPoint, &vfs) != 0 they
return a signed error sentinel (e.g., static_cast<int64_t>(-1)) instead of 0,
preserving the existing logic and types for the successful return path.

Fix critical 4-byte struct misalignment in RacModelInfoCStruct (missing
supports_lora field) causing memory corruption. Add complete LoRA FFI
bindings, bridge layer, public API, and types for Flutter feature parity
with iOS/Kotlin.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`:
- Around line 84-105: The LoRA methods (loadAdapter, removeAdapter,
clearAdapters, getLoadedAdapters) currently call
DartBridgeLLM.shared.getHandle() which can lazily create an empty LLM; add a
guard at the top of each of these methods to require a loaded base model before
proceeding (e.g. call a DartBridgeLLM.shared.isModelLoaded()/hasLoadedModel()
check or equivalent) and if no model is loaded throw a StateError with a clear
message instead of proceeding to getHandle(); for checkCompatibility, detect the
no-model case and return the incompatible/native error code (use the existing
RacResultCode/RAC_* conventions) rather than invoking native APIs. Ensure you
apply the same pattern to removeAdapter(), clearAdapters(), getLoadedAdapters(),
and checkCompatibility() so native calls are never reached when no base model is
loaded.
- Around line 349-369: Replace the native free lookup with the CRT-matching
rac_free and use it for all strdup'd strings: change the lookupFunction call
that currently resolves 'free' to resolve 'rac_free' (keeping the same typedefs
and the cFreeFn variable), and then call cFreeFn on entryPtr.ref.id, name,
description, downloadUrl, filename and on each non-null compatArrayPtr[i] in the
compat loop so all rac_strdup allocations are freed with rac_free.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart`:
- Around line 15-18: Replace the compile-time assert in the LoRAAdapterConfig
constructor with explicit runtime validation: in the LoRAAdapterConfig
constructor validate that path is not empty and throw an ArgumentError (or
ArgumentError.value) if it is, and also validate scale to ensure it is finite,
not NaN, and non-negative (throw ArgumentError if scale is NaN/Infinity or < 0);
perform these checks before assigning fields so invalid values never reach the
FFI boundary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71248ab6-9132-4fb2-b0d1-f6ca0c4be85d

📥 Commits

Reviewing files that changed from the base of the PR and between c2bc42f and 193dcb6.

📒 Files selected for processing (7)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_lora.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart

Comment on lines +84 to +105
void loadAdapter(String adapterPath, double scale) {
final handle = DartBridgeLLM.shared.getHandle();

final pathPtr = adapterPath.toNativeUtf8();
try {
final lib = PlatformLoader.loadCommons();
final fn = lib.lookupFunction<
Int32 Function(RacHandle, Pointer<Utf8>, Float),
int Function(RacHandle, Pointer<Utf8>, double)>(
'rac_llm_component_load_lora',
);

final result = fn(handle, pathPtr, scale);
if (result != RAC_SUCCESS) {
throw StateError(
'Failed to load LoRA adapter: ${RacResultCode.getMessage(result)}',
);
}
_logger.info('LoRA adapter loaded: $adapterPath (scale=$scale)');
} finally {
calloc.free(pathPtr);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Require a loaded base model before calling these LoRA runtime APIs.

These paths all call DartBridgeLLM.shared.getHandle() directly. Because that lazily creates the component, the public LoRA API can reach native calls before any base model is loaded. That makes load/remove/clear/check fail late against an empty LLM, and getLoadedAdapters() can allocate one just to return [].

Suggested guard pattern
   void loadAdapter(String adapterPath, double scale) {
+    if (!DartBridgeLLM.shared.isLoaded) {
+      throw StateError('No model loaded');
+    }
     final handle = DartBridgeLLM.shared.getHandle();
   List<LoRAAdapterInfo> getLoadedAdapters() {
+    if (!DartBridgeLLM.shared.isLoaded) return [];
     final handle = DartBridgeLLM.shared.getHandle();

Apply the same guard to removeAdapter() and clearAdapters(), and return an incompatible result from checkCompatibility() when no model is loaded.

Also applies to: 112-133, 140-157, 162-195, 198-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`
around lines 84 - 105, The LoRA methods (loadAdapter, removeAdapter,
clearAdapters, getLoadedAdapters) currently call
DartBridgeLLM.shared.getHandle() which can lazily create an empty LLM; add a
guard at the top of each of these methods to require a loaded base model before
proceeding (e.g. call a DartBridgeLLM.shared.isModelLoaded()/hasLoadedModel()
check or equivalent) and if no model is loaded throw a StateError with a clear
message instead of proceeding to getHandle(); for checkCompatibility, detect the
no-model case and return the incompatible/native error code (use the existing
RacResultCode/RAC_* conventions) rather than invoking native APIs. Ensure you
apply the same pattern to removeAdapter(), clearAdapters(), getLoadedAdapters(),
and checkCompatibility() so native calls are never reached when no base model is
loaded.

Comment on lines +349 to +369
final cFreeFn = lib.lookupFunction<
Void Function(Pointer<Void>),
void Function(Pointer<Void>)>('free');

// Free strdup'd strings inside the struct
if (entryPtr.ref.id != nullptr) cFreeFn(entryPtr.ref.id.cast());
if (entryPtr.ref.name != nullptr) cFreeFn(entryPtr.ref.name.cast());
if (entryPtr.ref.description != nullptr) {
cFreeFn(entryPtr.ref.description.cast());
}
if (entryPtr.ref.downloadUrl != nullptr) {
cFreeFn(entryPtr.ref.downloadUrl.cast());
}
if (entryPtr.ref.filename != nullptr) {
cFreeFn(entryPtr.ref.filename.cast());
}
// Free strdup'd compatible model IDs
for (int i = 0; i < compatCount; i++) {
if (compatArrayPtr[i] != nullptr) {
cFreeFn(compatArrayPtr[i].cast());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && find . -name "dart_bridge_lora.dart" -type f

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 108


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && wc -l lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 110


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '340,380p' lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1601


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && grep -n "loadCommons\|rac_free\|rac_strdup" lib/native/dart_bridge_lora.dart | head -30

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 662


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && rg "loadCommons" -A 10 -B 2 lib/native/

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 50387


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '180,195p' lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 511


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '217,235p' lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 648


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '280,295p' lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 654


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '300,330p' lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1366


Use rac_free to match the allocation CRT.

Strings allocated via rac_strdup (lines 287–320) must be freed via rac_free, not raw free. The pattern is already used successfully elsewhere in the file (lines 188, 225). Using free creates a CRT mismatch and risks heap corruption if it resolves through a different runtime than rac_strdup.

Suggested fix
-      final cFreeFn = lib.lookupFunction<
-          Void Function(Pointer<Void>),
-          void Function(Pointer<Void>)>('free');
+      final cFreeFn = lib.lookupFunction<
+          Void Function(Pointer<Void>),
+          void Function(Pointer<Void>)>('rac_free');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`
around lines 349 - 369, Replace the native free lookup with the CRT-matching
rac_free and use it for all strdup'd strings: change the lookupFunction call
that currently resolves 'free' to resolve 'rac_free' (keeping the same typedefs
and the cFreeFn variable), and then call cFreeFn on entryPtr.ref.id, name,
description, downloadUrl, filename and on each non-null compatArrayPtr[i] in the
compat loop so all rac_strdup allocations are freed with rac_free.

Comment on lines +15 to +18
LoRAAdapterConfig({
required this.path,
this.scale = 1.0,
}) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "lora_types.dart" | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 244


🏁 Script executed:

fd "lora_types.dart"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 151


🏁 Script executed:

cat -n "sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2877


🏁 Script executed:

rg -i "ffi|native" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -A 3 | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 5266


🏁 Script executed:

rg "LoRA|lora" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -B 2 -A 5

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 50388


🏁 Script executed:

rg "class.*Config\s*\{" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -A 10 | head -60

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6272


🌐 Web query:

Dart assert behavior release builds production Flutter

💡 Result:

  • In Flutter, build modes control whether assert(...) runs:

    • Debug mode: assertions are enabled. [1]
    • Release mode (production builds): assertions are disabled. [1]
    • Profile mode: is “similar to release mode”; in practice, Flutter’s engine artifacts/documentation also treat it as product mode with asserts disabled. [1][2]
  • In Dart, when assertions are disabled, the entire assert(...) is removed—the condition (and any side effects inside it) is not evaluated. So you must not put required logic inside an assert. [1]

  • If you need a check in production, use explicit runtime logic (e.g., if (...) throw ...;), not assert.

  • If you’re running/compiling Dart yourself and want assertions on, use --enable-asserts. [3]

Sources:
[1] Flutter docs: “Flutter’s build modes” (assertions enabled in debug, disabled in release) (docs.flutter.dev)
[2] Flutter engine doc noting asserts disabled in product/release-like builds (chromium.googlesource.com)
[3] Dart CLI docs (--enable-asserts) (dart.dev)

Citations:


🏁 Script executed:

sed -n '75,130p' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1841


Replace assert with explicit runtime validation for LoRAAdapterConfig.

assert is stripped in Flutter release builds, so an empty path will reach the FFI boundary unvalidated in production. Replace with ArgumentError and add validation for scale to prevent NaN/infinity/negative values.

Suggested fix
  LoRAAdapterConfig({
    required this.path,
    this.scale = 1.0,
-  }) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty');
+  }) {
+    if (path.isEmpty) {
+      throw ArgumentError.value(
+        path,
+        'path',
+        'LoRA adapter path cannot be empty',
+      );
+    }
+    if (!scale.isFinite || scale < 0) {
+      throw ArgumentError.value(
+        scale,
+        'scale',
+        'LoRA scale must be finite and non-negative',
+      );
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LoRAAdapterConfig({
required this.path,
this.scale = 1.0,
}) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty');
LoRAAdapterConfig({
required this.path,
this.scale = 1.0,
}) {
if (path.isEmpty) {
throw ArgumentError.value(
path,
'path',
'LoRA adapter path cannot be empty',
);
}
if (!scale.isFinite || scale < 0) {
throw ArgumentError.value(
scale,
'scale',
'LoRA scale must be finite and non-negative',
);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart`
around lines 15 - 18, Replace the compile-time assert in the LoRAAdapterConfig
constructor with explicit runtime validation: in the LoRAAdapterConfig
constructor validate that path is not empty and throw an ArgumentError (or
ArgumentError.value) if it is, and also validate scale to ensure it is finite,
not NaN, and non-negative (throw ArgumentError if scale is NaN/Infinity or < 0);
perform these checks before assigning fields so invalid values never reach the
FFI boundary.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart (1)

835-840: ⚠️ Potential issue | 🔴 Critical

Duplicate variable declaration causes compilation error.

Line 835 declares entriesPtr with calloc and line 836 re-declares it with malloc. Remove the original calloc line to fix.

🐛 Proposed fix
     // Allocate array of string pointers
-    final entriesPtr = calloc<Pointer<Utf8>>(entries.length);
     final entriesPtr = malloc<Pointer<Utf8>>(entries.length);
     for (var i = 0; i < entries.length; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart`
around lines 835 - 840, Remove the duplicate declaration of entriesPtr: delete
the calloc<Pointer<Utf8>>(entries.length) line and keep the
malloc<Pointer<Utf8>>(entries.length) allocation so entriesPtr is declared only
once; ensure the loop that writes entriesPtr[i] and the assignment
outEntries.value = entriesPtr remain unchanged and that the malloc symbol used
in dart_bridge_model_registry.dart matches existing memory allocation
conventions in the surrounding code.
♻️ Duplicate comments (3)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart (3)

275-278: ⚠️ Potential issue | 🟠 Major

Fail closed when storage availability can't be determined.

Returning true on a null result allows callers to proceed exactly when the bridge is missing or the native check failed.

Suggested fix
   static bool checkStorage(int requiredBytes) {
     final result = checkStorageAvailability(requiredBytes);
-    if (result == null) return true; // Default to available if check fails
+    if (result == null) return false;
     return result.isAvailable;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 275 - 278, The checkStorage method currently treats a null result
from checkStorageAvailability as available; change it to fail closed: if
checkStorageAvailability(requiredBytes) returns null, return false instead of
true so callers do not proceed when the native bridge is missing or the check
failed; update the null-handling branch in checkStorage (referencing
checkStorage and checkStorageAvailability and the result.isAvailable usage) to
return false and keep the normal return result.isAvailable for non-null results.

408-420: ⚠️ Potential issue | 🟡 Minor

Clean up partially allocated entry buffers on error.

If the loop throws after allocating entries and a few entries[i] strings, the catch path leaks both the array and the already-created strings.

Suggested fix
-    final entries = calloc<Pointer<Utf8>>(count);
-    for (var i = 0; i < count; i++) {
+    final entries = calloc<Pointer<Utf8>>(count);
+    var allocated = 0;
+    for (var i = 0; i < count; i++) {
       final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty);
-      entries[i] = name.toNativeUtf8();
+      entries[i] = name.toNativeUtf8(allocator: calloc);
+      allocated++;
     }
@@
-  } catch (_) {
+  } catch (_) {
+    for (var i = 0; i < allocated; i++) {
+      calloc.free(entries[i]);
+    }
+    calloc.free(entries);
     outEntries.value = nullptr;
     outCount.value = 0;
     return _errorFileNotFound;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 408 - 420, The catch path currently leaks memory because the
calloc'd entries array and any previously created Utf8 buffers (via
toNativeUtf8) are not freed; modify the function so that if an exception occurs
after allocating entries, you iterate from 0 to the last successfully
initialized index and free each entries[i] (using malloc.free or appropriate
freeUtf8), then free the entries array itself before setting outEntries.value =
nullptr and outCount.value = 0 and returning _errorFileNotFound; ensure the same
cleanup logic is used for any early-return error paths so that the
calloc<Pointer<Utf8>>(count) allocation and any populated entries[i] are always
released (reference symbols: entries, calloc<Pointer<Utf8>>, toNativeUtf8,
outEntries, outCount, _errorFileNotFound, RacResultCode.success).

104-106: ⚠️ Potential issue | 🟠 Major

Don't collapse native file-manager failures into 0.

These methods ignore the native return code and always read the out-param, so an FFI failure is indistinguishable from a real empty directory/cache. Surface the error, or return null, and only read the size when the native call succeeds.

Fix pattern
-      fn(_callbacksPtr!, pathPtr, sizePtr);
-      return sizePtr.value;
+      final result = fn(_callbacksPtr!, pathPtr, sizePtr);
+      if (result != RacResultCode.success) {
+        throw StateError('rac_file_manager_calculate_dir_size failed: $result');
+      }
+      return sizePtr.value;

Also applies to: 124-126, 163-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 104 - 106, The current try-block calls the native callback
fn(_callbacksPtr!, pathPtr, sizePtr) and always returns sizePtr.value,
collapsing native failures into 0; change the logic to capture and check the
native return code from fn, and only read and return sizePtr.value when the
native call indicates success (otherwise return null or propagate the error).
Update the occurrence that uses fn, _callbacksPtr, pathPtr, sizePtr (and the
analogous sites mentioned for lines 124-126 and 163-165) so they inspect the
native return value before reading the out-parameter, returning null or throwing
when the native call failed.
🧹 Nitpick comments (2)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart (1)

176-176: Replace the raw archive-structure sentinel with a named bridge constant.

Hardcoding 99 here creates a silent Dart↔C++ enum dependency. If the native value ever changes, auto-detection breaks without any compiler help. Please expose/use a named constant alongside the existing model.format.toC() bridge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`
at line 176, The code uses a hardcoded sentinel value `99` for `structure` which
creates a brittle Dart↔C++ enum dependency; add and use a named bridge constant
(e.g. `ARCHIVE_STRUCTURE_UNKNOWN` or `RAC_ARCHIVE_STRUCTURE_UNKNOWN`) exported
from the same bridge surface as `model.format.toC()` and replace `structure: 99`
with `structure: <BRIDGE_CONSTANT>` so the Dart side references the named
constant instead of the raw number; ensure the constant name matches the native
enum sentinel and is documented alongside `model.format.toC()` so changes in
native values won’t silently break auto-detection.
sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (1)

377-381: Replace the raw archive-structure sentinel with a shared constant.

99 is a native-side enum value embedded directly in Dart. If that value ever drifts in the C API, model-path discovery here will silently use the wrong mode. Please export a named constant through the bridge/FFI layer and use that symbol instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 377 - 381, The call to
DartBridgeDownload.findModelPathAfterExtraction uses a hardcoded sentinel "99"
for the structure argument which can drift if the native enum changes; export a
named constant from the bridge/FFI layer (e.g., RAC_ARCHIVE_STRUCTURE_UNKNOWN)
and replace the literal with that constant so the call becomes:
DartBridgeDownload.findModelPathAfterExtraction(extractedDir: destDir,
structure: BridgeConstants.RAC_ARCHIVE_STRUCTURE_UNKNOWN, framework:
framework.toC(), format: format.toC()); ensure the constant is defined and
exported by the Dart FFI bridge module and referenced here instead of the raw
99.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 102-109: The code calls toNativeUtf8() (producing pathPtr) but
frees the pointer with calloc.free(), violating allocator symmetry; update each
toNativeUtf8() call in dart_bridge_file_manager.dart to specify allocator:
calloc so the pointer is allocated with calloc and can be freed with
calloc.free() (apply this change for all occurrences that create pathPtr and
later call calloc.free(), e.g., the places that define pathPtr/sizePtr and later
free them).

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`:
- Around line 174-181: The current resolveModelFilePath implementation masks
extraction/layout failures by returning modelFolder when
DartBridgeDownload.findModelPathAfterExtraction returns null; change the code to
stop falling back and return the resolved value directly (replace "return
resolved ?? modelFolder;" with "return resolved;"), ensure
resolveModelFilePath's return type is nullable if not already, and update
callers to handle a null result from
DartBridgeDownload.findModelPathAfterExtraction/resolveModelFilePath so
single-file formats (e.g., GGUF) and extraction errors are not misreported as a
directory path.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`:
- Around line 173-177: The UTF-8 conversion calls (e.g., when setting
cConfig.ref.promptTemplate, cConfig.ref.embeddingConfigJson, and
cConfig.ref.llmConfigJson) use toNativeUtf8() which defaults to malloc, but the
code later frees those pointers with calloc.free(); change these to
toNativeUtf8(allocator: calloc) so the allocator matches the cleanup for all
UTF-8 strings in the function (apply the same change to the other occurrences
noted).

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart`:
- Around line 96-99: The code marks the module registered unconditionally after
calling DartBridgeRAG.shared.register(); change the flow so _isRegistered is
only set to true (and _logger.info called) when DartBridgeRAG.shared.register()
confirms success—either by returning a success boolean or by throwing on
failure; if register currently only logs and returns, update
DartBridgeRAG.register() to return/throw success/failure and then check that
result here, leaving _isRegistered false and logging an error when the bridge
did not start successfully. Ensure you reference
DartBridgeRAG.shared.register(), _isRegistered, and the surrounding registration
logic when making the change.

---

Outside diff comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart`:
- Around line 835-840: Remove the duplicate declaration of entriesPtr: delete
the calloc<Pointer<Utf8>>(entries.length) line and keep the
malloc<Pointer<Utf8>>(entries.length) allocation so entriesPtr is declared only
once; ensure the loop that writes entriesPtr[i] and the assignment
outEntries.value = entriesPtr remain unchanged and that the malloc symbol used
in dart_bridge_model_registry.dart matches existing memory allocation
conventions in the surrounding code.

---

Duplicate comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 275-278: The checkStorage method currently treats a null result
from checkStorageAvailability as available; change it to fail closed: if
checkStorageAvailability(requiredBytes) returns null, return false instead of
true so callers do not proceed when the native bridge is missing or the check
failed; update the null-handling branch in checkStorage (referencing
checkStorage and checkStorageAvailability and the result.isAvailable usage) to
return false and keep the normal return result.isAvailable for non-null results.
- Around line 408-420: The catch path currently leaks memory because the
calloc'd entries array and any previously created Utf8 buffers (via
toNativeUtf8) are not freed; modify the function so that if an exception occurs
after allocating entries, you iterate from 0 to the last successfully
initialized index and free each entries[i] (using malloc.free or appropriate
freeUtf8), then free the entries array itself before setting outEntries.value =
nullptr and outCount.value = 0 and returning _errorFileNotFound; ensure the same
cleanup logic is used for any early-return error paths so that the
calloc<Pointer<Utf8>>(count) allocation and any populated entries[i] are always
released (reference symbols: entries, calloc<Pointer<Utf8>>, toNativeUtf8,
outEntries, outCount, _errorFileNotFound, RacResultCode.success).
- Around line 104-106: The current try-block calls the native callback
fn(_callbacksPtr!, pathPtr, sizePtr) and always returns sizePtr.value,
collapsing native failures into 0; change the logic to capture and check the
native return code from fn, and only read and return sizePtr.value when the
native call indicates success (otherwise return null or propagate the error).
Update the occurrence that uses fn, _callbacksPtr, pathPtr, sizePtr (and the
analogous sites mentioned for lines 124-126 and 163-165) so they inspect the
native return value before reading the out-parameter, returning null or throwing
when the native call failed.

---

Nitpick comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 377-381: The call to
DartBridgeDownload.findModelPathAfterExtraction uses a hardcoded sentinel "99"
for the structure argument which can drift if the native enum changes; export a
named constant from the bridge/FFI layer (e.g., RAC_ARCHIVE_STRUCTURE_UNKNOWN)
and replace the literal with that constant so the call becomes:
DartBridgeDownload.findModelPathAfterExtraction(extractedDir: destDir,
structure: BridgeConstants.RAC_ARCHIVE_STRUCTURE_UNKNOWN, framework:
framework.toC(), format: format.toC()); ensure the constant is defined and
exported by the Dart FFI bridge module and referenced here instead of the raw
99.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`:
- Line 176: The code uses a hardcoded sentinel value `99` for `structure` which
creates a brittle Dart↔C++ enum dependency; add and use a named bridge constant
(e.g. `ARCHIVE_STRUCTURE_UNKNOWN` or `RAC_ARCHIVE_STRUCTURE_UNKNOWN`) exported
from the same bridge surface as `model.format.toC()` and replace `structure: 99`
with `structure: <BRIDGE_CONSTANT>` so the Dart side references the named
constant instead of the raw number; ensure the constant name matches the native
enum sentinel and is documented alongside `model.format.toC()` so changes in
native values won’t silently break auto-detection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d33f0c08-b945-49c2-95a5-df4bbd4372b3

📥 Commits

Reviewing files that changed from the base of the PR and between 193dcb6 and 6b38fb0.

📒 Files selected for processing (9)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_rag.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/rag_types.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
💤 Files with no reviewable changes (1)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/rag_types.dart

Comment on lines +102 to +109
final pathPtr = path.toNativeUtf8();
final sizePtr = calloc<Int64>();
try {
fn(_callbacksPtr!, pathPtr, sizePtr);
return sizePtr.value;
} finally {
calloc.free(pathPtr);
calloc.free(sizePtr);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Dart package:ffi String.toNativeUtf8() allocate with malloc by default, and must the returned pointer be freed with the same allocator instead of calloc.free()?

💡 Result:

Yes.

  • String.toNativeUtf8() is declared as toNativeUtf8({Allocator allocator = malloc}), so it allocates with malloc by default. [1]
  • The returned pointer should be freed with the same allocator you used to allocate it (i.e., allocator.free(ptr)). With the default, that means malloc.free(ptr). [1][2]

In practice, calloc.free(ptr) often “works” because both allocators typically route free to the same underlying native deallocator, but relying on that is brittle—especially if you ever pass a different/custom allocator—so prefer malloc.free (or allocator.free). [1][2]

Sources: [1] StringUtf8Pointer.toNativeUtf8 API docs, [2] malloc allocator docs.


Free these UTF-8 buffers with the allocator that created them.

toNativeUtf8() defaults to malloc, but every cleanup path uses calloc.free(). The pointer must be freed with the same allocator that allocated it. While both allocators typically route to the same underlying deallocator, this mismatch violates the allocator contract and is brittle—especially if custom allocators are ever used.

Specify allocator: calloc in toNativeUtf8() calls to match the corresponding calloc.free() cleanup, or switch cleanup to malloc.free().

Fix pattern
-    final pathPtr = path.toNativeUtf8();
+    final pathPtr = path.toNativeUtf8(allocator: calloc);
...
-    final modelIdPtr = modelId.toNativeUtf8();
+    final modelIdPtr = modelId.toNativeUtf8(allocator: calloc);
...
-      entries[i] = name.toNativeUtf8();
+      entries[i] = name.toNativeUtf8(allocator: calloc);

Applies to lines 102–109, 181–190, 205–212, 291–296, 408–429.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 102 - 109, The code calls toNativeUtf8() (producing pathPtr) but
frees the pointer with calloc.free(), violating allocator symmetry; update each
toNativeUtf8() call in dart_bridge_file_manager.dart to specify allocator:
calloc so the pointer is allocated with calloc and can be freed with
calloc.free() (apply this change for all occurrences that create pathPtr and
later call calloc.free(), e.g., the places that define pathPtr/sizePtr and later
free them).

Comment on lines +174 to +181
final resolved = DartBridgeDownload.findModelPathAfterExtraction(
extractedDir: modelFolder,
structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
framework: _frameworkToCValue(model.framework),
format: model.format.toC(),
);

// Scan for any subdirectory with model files
final dir = Directory(modelFolder);
if (dir.existsSync()) {
for (final entity in dir.listSync()) {
if (entity is Directory && _hasONNXModelFiles(entity.path)) {
_logger.info('Found ONNX model in subdirectory: ${entity.path}');
return entity.path;
}
}
}

// Fallback
_logger.warning('No ONNX model files found, using: $modelFolder');
return modelFolder;
}

/// Check if directory contains ONNX model files
bool _hasONNXModelFiles(String directory) {
final dir = Directory(directory);
if (!dir.existsSync()) return false;

try {
return dir.listSync().any((entity) {
if (entity is! File) return false;
final name = entity.path.toLowerCase();
return name.endsWith('.onnx') ||
name.endsWith('.ort') ||
name.contains('encoder') ||
name.contains('decoder') ||
name.contains('tokens');
});
} catch (e) {
return false;
}
}

/// Resolve single-file model path (LlamaCpp .gguf files)
String? _resolveSingleFileModelPath(String modelFolder, ModelInfo model) {
final dir = Directory(modelFolder);
if (!dir.existsSync()) {
_logger.warning('Model folder does not exist: $modelFolder');
return null;
}

// Find the model file
try {
for (final entity in dir.listSync()) {
if (entity is File) {
final name = entity.path.toLowerCase();
if (name.endsWith('.gguf') || name.endsWith('.bin')) {
_logger.info('Found model file: ${entity.path}');
return entity.path;
}
}
}
} catch (e) {
_logger.warning('Error scanning model folder: $e');
}

_logger.warning('No model file found in: $modelFolder');
return null;
return resolved ?? modelFolder;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not fall back to the model directory when native resolution fails.

resolveModelFilePath() is documented as returning the loadable model path, but Line 181 returns the container directory for every unresolved case. That masks extraction/layout failures and is wrong for single-file formats like GGUF, where callers will receive a directory instead of a model file.

Suggested fix
     final resolved = DartBridgeDownload.findModelPathAfterExtraction(
       extractedDir: modelFolder,
       structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
       framework: _frameworkToCValue(model.framework),
       format: model.format.toC(),
     );

-    return resolved ?? modelFolder;
+    if (resolved != null) {
+      return resolved;
+    }
+
+    _logger.warning(
+      'Unable to resolve extracted model path for ${model.id} in $modelFolder',
+    );
+    return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final resolved = DartBridgeDownload.findModelPathAfterExtraction(
extractedDir: modelFolder,
structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
framework: _frameworkToCValue(model.framework),
format: model.format.toC(),
);
// Scan for any subdirectory with model files
final dir = Directory(modelFolder);
if (dir.existsSync()) {
for (final entity in dir.listSync()) {
if (entity is Directory && _hasONNXModelFiles(entity.path)) {
_logger.info('Found ONNX model in subdirectory: ${entity.path}');
return entity.path;
}
}
}
// Fallback
_logger.warning('No ONNX model files found, using: $modelFolder');
return modelFolder;
}
/// Check if directory contains ONNX model files
bool _hasONNXModelFiles(String directory) {
final dir = Directory(directory);
if (!dir.existsSync()) return false;
try {
return dir.listSync().any((entity) {
if (entity is! File) return false;
final name = entity.path.toLowerCase();
return name.endsWith('.onnx') ||
name.endsWith('.ort') ||
name.contains('encoder') ||
name.contains('decoder') ||
name.contains('tokens');
});
} catch (e) {
return false;
}
}
/// Resolve single-file model path (LlamaCpp .gguf files)
String? _resolveSingleFileModelPath(String modelFolder, ModelInfo model) {
final dir = Directory(modelFolder);
if (!dir.existsSync()) {
_logger.warning('Model folder does not exist: $modelFolder');
return null;
}
// Find the model file
try {
for (final entity in dir.listSync()) {
if (entity is File) {
final name = entity.path.toLowerCase();
if (name.endsWith('.gguf') || name.endsWith('.bin')) {
_logger.info('Found model file: ${entity.path}');
return entity.path;
}
}
}
} catch (e) {
_logger.warning('Error scanning model folder: $e');
}
_logger.warning('No model file found in: $modelFolder');
return null;
return resolved ?? modelFolder;
final resolved = DartBridgeDownload.findModelPathAfterExtraction(
extractedDir: modelFolder,
structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
framework: _frameworkToCValue(model.framework),
format: model.format.toC(),
);
if (resolved != null) {
return resolved;
}
_logger.warning(
'Unable to resolve extracted model path for ${model.id} in $modelFolder',
);
return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`
around lines 174 - 181, The current resolveModelFilePath implementation masks
extraction/layout failures by returning modelFolder when
DartBridgeDownload.findModelPathAfterExtraction returns null; change the code to
stop falling back and return the resolved value directly (replace "return
resolved ?? modelFolder;" with "return resolved;"), ensure
resolveModelFilePath's return type is nullable if not already, and update
callers to handle a null result from
DartBridgeDownload.findModelPathAfterExtraction/resolveModelFilePath so
single-file formats (e.g., GGUF) and extraction errors are not misreported as a
directory path.

Comment on lines +173 to +177
cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null
? config.embeddingConfigJSON!.toNativeUtf8()
: nullptr;
cConfig.ref.llmConfigJson = config.llmConfigJson != null
? config.llmConfigJson!.toNativeUtf8()
cConfig.ref.llmConfigJson = config.llmConfigJSON != null
? config.llmConfigJSON!.toNativeUtf8()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Dart package:ffi String.toNativeUtf8() allocate with malloc by default, and must the returned pointer be freed with the same allocator instead of calloc.free()?

💡 Result:

Yes.

  • String.toNativeUtf8() (from package:ffi/ffi.dart) has an optional allocator parameter that defaults to malloc ({Allocator allocator = malloc}), and it returns an allocator-allocated pointer. [1]
  • Therefore, if you use the default, free it with the same allocator: malloc.free(ptr). [1][2]

Using calloc.free(ptr) is typically equivalent on the supported platforms because both allocators ultimately release memory with the platform “free” routine (POSIX: free; Windows: CoTaskMemFree). [2][3] But the correct rule is: free with the same allocator you allocated with (and this matters if you pass a different/custom allocator). [1]

Sources
[1] https://pub.dev/documentation/ffi/latest/ffi/StringUtf8Pointer/toNativeUtf8.html
[2] https://pub.dev/documentation/ffi/latest/ffi/MallocAllocator-class.html
[3] https://pub.dev/documentation/ffi/latest/ffi/CallocAllocator-class.html


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere/lib/native && wc -l dart_bridge_rag.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 98


🏁 Script executed:

cd sdk/runanywhere-flutter/packages/runanywhere/lib/native && sed -n '170,210p' dart_bridge_rag.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1448


Match the allocator for these JSON config buffers.

toNativeUtf8() uses malloc by default, but these pointers are released with calloc.free(). While this may work in practice because both allocators typically use the same underlying platform free routine, it violates the allocator contract and is not guaranteed to be safe across all platforms. Use allocator: calloc here to match the cleanup, and apply the same fix to promptTemplate (line 171) and other UTF-8 strings in this function that follow the same pattern.

Fix pattern
-      cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null
-          ? config.embeddingConfigJSON!.toNativeUtf8()
+      cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null
+          ? config.embeddingConfigJSON!.toNativeUtf8(allocator: calloc)
           : nullptr;
-      cConfig.ref.llmConfigJson = config.llmConfigJSON != null
-          ? config.llmConfigJSON!.toNativeUtf8()
+      cConfig.ref.llmConfigJson = config.llmConfigJSON != null
+          ? config.llmConfigJSON!.toNativeUtf8(allocator: calloc)
           : nullptr;

Also applies to: 171, 197–201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`
around lines 173 - 177, The UTF-8 conversion calls (e.g., when setting
cConfig.ref.promptTemplate, cConfig.ref.embeddingConfigJson, and
cConfig.ref.llmConfigJson) use toNativeUtf8() which defaults to malloc, but the
code later frees those pointers with calloc.free(); change these to
toNativeUtf8(allocator: calloc) so the allocator matches the cleanup for all
UTF-8 strings in the function (apply the same change to the other occurrences
noted).

Comment on lines +96 to 99
DartBridgeRAG.shared.register();

_isRegistered = true;
_logger.info('RAG backend registered successfully');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only mark the module registered after the bridge confirms success.

DartBridgeRAG.shared.register() does not throw on a non-success native return; it just logs and returns. This path still flips _isRegistered to true, so callers can bypass the registration guard even though the backend never came up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart`
around lines 96 - 99, The code marks the module registered unconditionally after
calling DartBridgeRAG.shared.register(); change the flow so _isRegistered is
only set to true (and _logger.info called) when DartBridgeRAG.shared.register()
confirms success—either by returning a success boolean or by throwing on
failure; if register currently only logs and returns, update
DartBridgeRAG.register() to return/throw success/failure and then check that
result here, leaving _isRegistered false and logging an error when the bridge
did not start successfully. Ensure you reference
DartBridgeRAG.shared.register(), _isRegistered, and the surrounding registration
logic when making the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants